From 7788a29a966e6300af857015c59e5e2edae96532 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Nov 2022 14:17:56 +0100 Subject: [PATCH 01/23] Make BindingExpression and ExpressionObserver internal. --- src/Avalonia.Base/Avalonia.Base.csproj | 3 +++ src/Avalonia.Base/Data/Core/BindingExpression.cs | 2 +- src/Avalonia.Base/Data/Core/ExpressionObserver.cs | 2 +- .../MarkupExtensions/CompiledBindingExtension.cs | 2 +- src/Markup/Avalonia.Markup/Avalonia.Markup.csproj | 1 + src/Markup/Avalonia.Markup/Data/Binding.cs | 2 +- src/Markup/Avalonia.Markup/Data/BindingBase.cs | 12 ++++++------ .../Markup/Parsers/ExpressionObserverBuilder.cs | 2 +- 8 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index cd122a8b67..4a67191132 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -47,6 +47,9 @@ + + + diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index 55caf8070e..79942cb9ce 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -13,7 +13,7 @@ namespace Avalonia.Data.Core /// that are sent and received. /// [RequiresUnreferencedCode(TrimmingMessages.TypeConvertionRequiresUnreferencedCodeMessage)] - public class BindingExpression : LightweightObservableBase, IAvaloniaSubject, IDescription + internal class BindingExpression : LightweightObservableBase, IAvaloniaSubject, IDescription { private readonly ExpressionObserver _inner; private readonly Type _targetType; diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 0818b5fa62..ce3549c4ad 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -11,7 +11,7 @@ namespace Avalonia.Data.Core /// /// Observes and sets the value of an expression on an object. /// - public class ExpressionObserver : LightweightObservableBase, IDescription + internal class ExpressionObserver : LightweightObservableBase, IDescription { /// /// An ordered collection of property accessor plugins that can be used to customize diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs index 9990ad4731..d0a4de09ab 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -35,7 +35,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions }; } - protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) + private protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { if (Source != null) { diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index e3878b5bc6..ec44eeb38f 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -19,6 +19,7 @@ + diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 66907f33d0..37e7be5e3f 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -56,7 +56,7 @@ namespace Avalonia.Data /// public Func? TypeResolver { get; set; } - protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor, bool enableDataValidation) + private protected override ExpressionObserver CreateExpressionObserver(AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor, bool enableDataValidation) { _ = target ?? throw new ArgumentNullException(nameof(target)); diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs index 90f312a249..8c8c50763b 100644 --- a/src/Markup/Avalonia.Markup/Data/BindingBase.cs +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -70,7 +70,7 @@ namespace Avalonia.Data public WeakReference? NameScope { get; set; } - protected abstract ExpressionObserver CreateExpressionObserver( + private protected abstract ExpressionObserver CreateExpressionObserver( AvaloniaObject target, AvaloniaProperty? targetProperty, object? anchor, @@ -127,7 +127,7 @@ namespace Avalonia.Data return new InstancedBinding(subject, Mode, Priority); } - protected ExpressionObserver CreateDataContextObserver( + private protected ExpressionObserver CreateDataContextObserver( AvaloniaObject target, ExpressionNode node, bool targetIsDataContext, @@ -162,7 +162,7 @@ namespace Avalonia.Data } } - protected ExpressionObserver CreateElementObserver( + private protected ExpressionObserver CreateElementObserver( StyledElement target, string elementName, ExpressionNode node) @@ -178,7 +178,7 @@ namespace Avalonia.Data return result; } - protected ExpressionObserver CreateFindAncestorObserver( + private protected ExpressionObserver CreateFindAncestorObserver( StyledElement target, RelativeSource relativeSource, ExpressionNode node) @@ -211,7 +211,7 @@ namespace Avalonia.Data null); } - protected ExpressionObserver CreateSourceObserver( + private protected ExpressionObserver CreateSourceObserver( object source, ExpressionNode node) { @@ -220,7 +220,7 @@ namespace Avalonia.Data return new ExpressionObserver(source, node); } - protected ExpressionObserver CreateTemplatedParentObserver( + private protected ExpressionObserver CreateTemplatedParentObserver( AvaloniaObject target, ExpressionNode node) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 00f40dfcd3..97e9698c13 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -6,7 +6,7 @@ using Avalonia.Utilities; namespace Avalonia.Markup.Parsers { - public static class ExpressionObserverBuilder + internal static class ExpressionObserverBuilder { [RequiresUnreferencedCode(TrimmingMessages.ReflectionBindingRequiresUnreferencedCodeMessage)] internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func? typeResolver = null, From cb8a21fb836aeab380541c07b58845005a598b11 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 30 Nov 2022 14:51:13 +0100 Subject: [PATCH 02/23] Make ExpressionNode (and derived) internal. --- src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs | 2 +- src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs | 2 +- src/Avalonia.Base/Data/Core/ExpressionNode.cs | 2 +- src/Avalonia.Base/Data/Core/IndexerNodeBase.cs | 2 +- src/Avalonia.Base/Data/Core/LogicalNotNode.cs | 2 +- src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs | 2 +- src/Avalonia.Base/Data/Core/SettableNode.cs | 2 +- src/Avalonia.Base/Data/Core/StreamNode.cs | 2 +- src/Avalonia.Base/Data/Core/TypeCastNode.cs | 2 +- .../MarkupExtensions/CompiledBindings/CompiledBindingPath.cs | 2 +- .../MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs | 2 +- .../Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs | 2 +- .../Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs | 2 +- src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs index 536c14dcf9..92fc843394 100644 --- a/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/AvaloniaPropertyAccessorNode.cs @@ -3,7 +3,7 @@ using Avalonia.Reactive; namespace Avalonia.Data.Core { - public class AvaloniaPropertyAccessorNode : SettableNode + internal class AvaloniaPropertyAccessorNode : SettableNode { private IDisposable? _subscription; private readonly bool _enableValidation; diff --git a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs index 4e142fbee9..b333fa9047 100644 --- a/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/EmptyExpressionNode.cs @@ -1,6 +1,6 @@ namespace Avalonia.Data.Core { - public class EmptyExpressionNode : ExpressionNode + internal class EmptyExpressionNode : ExpressionNode { public override string Description => "."; } diff --git a/src/Avalonia.Base/Data/Core/ExpressionNode.cs b/src/Avalonia.Base/Data/Core/ExpressionNode.cs index 5fb2bb5c13..30fc71cfb4 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionNode.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionNode.cs @@ -2,7 +2,7 @@ using System; namespace Avalonia.Data.Core { - public abstract class ExpressionNode + internal abstract class ExpressionNode { protected static readonly WeakReference UnsetReference = new WeakReference(AvaloniaProperty.UnsetValue); diff --git a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs index 2fad96701d..9ec256225b 100644 --- a/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs +++ b/src/Avalonia.Base/Data/Core/IndexerNodeBase.cs @@ -7,7 +7,7 @@ using Avalonia.Utilities; namespace Avalonia.Data.Core { - public abstract class IndexerNodeBase : SettableNode, + internal abstract class IndexerNodeBase : SettableNode, IWeakEventSubscriber, IWeakEventSubscriber { diff --git a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs index 45837db73d..81b07bfe85 100644 --- a/src/Avalonia.Base/Data/Core/LogicalNotNode.cs +++ b/src/Avalonia.Base/Data/Core/LogicalNotNode.cs @@ -3,7 +3,7 @@ using System.Globalization; namespace Avalonia.Data.Core { - public class LogicalNotNode : ExpressionNode, ITransformNode + internal class LogicalNotNode : ExpressionNode, ITransformNode { public override string Description => "!"; diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index 1b79fed6e7..3898d232ec 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -5,7 +5,7 @@ using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] - public class PropertyAccessorNode : SettableNode + internal class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; private IPropertyAccessorPlugin? _customPlugin; diff --git a/src/Avalonia.Base/Data/Core/SettableNode.cs b/src/Avalonia.Base/Data/Core/SettableNode.cs index 9ad9ace814..4980e4487a 100644 --- a/src/Avalonia.Base/Data/Core/SettableNode.cs +++ b/src/Avalonia.Base/Data/Core/SettableNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Data.Core { - public abstract class SettableNode : ExpressionNode + internal abstract class SettableNode : ExpressionNode { public bool SetTargetValue(object? value, BindingPriority priority) { diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index ba18a2173b..d3da6414ac 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -6,7 +6,7 @@ using Avalonia.Reactive; namespace Avalonia.Data.Core { [RequiresUnreferencedCode(TrimmingMessages.ExpressionNodeRequiresUnreferencedCodeMessage)] - public class StreamNode : ExpressionNode + internal class StreamNode : ExpressionNode { private IStreamPlugin? _customPlugin = null; private IDisposable? _subscription; diff --git a/src/Avalonia.Base/Data/Core/TypeCastNode.cs b/src/Avalonia.Base/Data/Core/TypeCastNode.cs index 3a2ca955fa..655bfbc7a1 100644 --- a/src/Avalonia.Base/Data/Core/TypeCastNode.cs +++ b/src/Avalonia.Base/Data/Core/TypeCastNode.cs @@ -4,7 +4,7 @@ using System.Text; namespace Avalonia.Data.Core { - public class TypeCastNode : ExpressionNode + internal class TypeCastNode : ExpressionNode { public override string Description => $"as {TargetType.FullName}"; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs index b7f2261324..2b62d33349 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -24,7 +24,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings } [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = TrimmingMessages.CompiledBindingSafeSupressWarningMessage)] - public ExpressionNode BuildExpression(bool enableValidation) + internal ExpressionNode BuildExpression(bool enableValidation) { ExpressionNode pathRoot = null; ExpressionNode path = null; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs index 1252ec7eca..b543fd9c01 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/StrongTypeCastNode.cs @@ -3,7 +3,7 @@ using Avalonia.Data.Core; namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings { - public class StrongTypeCastNode : TypeCastNode + internal class StrongTypeCastNode : TypeCastNode { private Func _cast; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index 4fc17e440b..e676c74879 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -6,7 +6,7 @@ using Avalonia.Reactive; namespace Avalonia.Markup.Parsers.Nodes { - public class ElementNameNode : ExpressionNode + internal class ElementNameNode : ExpressionNode { private readonly WeakReference _nameScope; private readonly string _name; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index ffbd34d492..383a160814 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -5,7 +5,7 @@ using Avalonia.Reactive; namespace Avalonia.Markup.Parsers.Nodes { - public class FindAncestorNode : ExpressionNode + internal class FindAncestorNode : ExpressionNode { private readonly int _level; private readonly Type? _ancestorType; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs index 1cd233c68a..2cb87efa65 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Markup.Parsers.Nodes { - public class SelfNode : ExpressionNode + internal class SelfNode : ExpressionNode { public override string Description => "$self"; } From 67c9221d3cf550bb17b2ad248bb7bf8b3acda858 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Dec 2022 15:21:24 +0100 Subject: [PATCH 03/23] Tweaked InstancedBinding API. - Remove `Value` from the API, will always contain an `IObservable` from now - Remove subject from the API, caller can try to cast the observable itself --- src/Avalonia.Base/Data/BindingOperations.cs | 59 ++++++++----------- src/Avalonia.Base/Data/InstancedBinding.cs | 43 +++++++------- src/Avalonia.Base/Styling/Setter.cs | 2 +- .../DataGridBoundColumn.cs | 5 +- .../Avalonia.Markup/Data/MultiBinding.cs | 4 +- .../Data/BindingTests.cs | 6 +- .../Data/BindingTests_Converters.cs | 8 +-- .../Data/BindingTests_DataValidation.cs | 6 +- .../Data/MultiBindingTests.cs | 4 +- 9 files changed, 63 insertions(+), 74 deletions(-) diff --git a/src/Avalonia.Base/Data/BindingOperations.cs b/src/Avalonia.Base/Data/BindingOperations.cs index 0b737dd959..e53ae40cb1 100644 --- a/src/Avalonia.Base/Data/BindingOperations.cs +++ b/src/Avalonia.Base/Data/BindingOperations.cs @@ -41,54 +41,41 @@ namespace Avalonia.Data { case BindingMode.Default: case BindingMode.OneWay: - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - return target.Bind(property, binding.Observable, binding.Priority); + return target.Bind(property, binding.Source, binding.Priority); case BindingMode.TwoWay: - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - if (binding.Subject is null) + { + if (binding.Source is not IObserver observer) throw new InvalidOperationException("InstancedBinding does not contain a subject."); return new TwoWayBindingDisposable( - target.Bind(property, binding.Observable, binding.Priority), - target.GetObservable(property).Subscribe(binding.Subject)); + target.Bind(property, binding.Source, binding.Priority), + target.GetObservable(property).Subscribe(observer)); + } case BindingMode.OneTime: - if (binding.Observable is {} source) - { - // Perf: Avoid allocating closure in the outer scope. - var targetCopy = target; - var propertyCopy = property; - var bindingCopy = binding; - - return source - .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) - .Take(1) - .Subscribe(x => targetCopy.SetValue( - propertyCopy, - BindingNotification.ExtractValue(x), - bindingCopy.Priority)); - } - else - { - target.SetValue(property, binding.Value, binding.Priority); - return Disposable.Empty; - } + { + // Perf: Avoid allocating closure in the outer scope. + var targetCopy = target; + var propertyCopy = property; + var bindingCopy = binding; + + return binding.Source + .Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue) + .Take(1) + .Subscribe(x => targetCopy.SetValue( + propertyCopy, + BindingNotification.ExtractValue(x), + bindingCopy.Priority)); + } case BindingMode.OneWayToSource: { - if (binding.Observable is null) - throw new InvalidOperationException("InstancedBinding does not contain an observable."); - if (binding.Subject is null) + if (binding.Source is not IObserver observer) throw new InvalidOperationException("InstancedBinding does not contain a subject."); - // Perf: Avoid allocating closure in the outer scope. - var bindingCopy = binding; - return Observable.CombineLatest( - binding.Observable, + binding.Source, target.GetObservable(property), (_, v) => v) - .Subscribe(x => bindingCopy.Subject.OnNext(x)); + .Subscribe(x => observer.OnNext(x)); } default: diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index a60c1d72ec..00e5c3d8e6 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Reactive; +using ObservableEx = Avalonia.Reactive.Observable; namespace Avalonia.Data { @@ -14,11 +15,23 @@ namespace Avalonia.Data /// public class InstancedBinding { - internal InstancedBinding(object? value, BindingMode mode, BindingPriority priority) + /// + /// Initializes a new instance of the class. + /// + /// The binding source. + /// The binding mode. + /// The priority of the binding. + /// + /// This constructor can be used to create any type of binding and as such requires an + /// as the binding source because this is the only binding + /// source which can be used for all binding modes. If you wish to create an instance with + /// something other than a subject, use one of the static creation methods on this class. + /// + internal InstancedBinding(IObservable source, BindingMode mode, BindingPriority priority) { Mode = mode; Priority = priority; - Value = value; + Source = source ?? throw new ArgumentNullException(nameof(source)); } /// @@ -32,24 +45,12 @@ namespace Avalonia.Data public BindingPriority Priority { get; } /// - /// Gets the value or source of the binding. - /// - public object? Value { get; } - - /// - /// Gets the as an observable. + /// Gets the binding source observable. /// - public IObservable? Observable => Value as IObservable; + public IObservable Source { get; } - /// - /// Gets the as an observer. - /// - public IObserver? Observer => Value as IObserver; - - /// - /// Gets the as an subject. - /// - internal IAvaloniaSubject? Subject => Value as IAvaloniaSubject; + [Obsolete("Use Source property")] + public IObservable Observable => Source; /// /// Creates a new one-time binding with a fixed value. @@ -61,7 +62,7 @@ namespace Avalonia.Data object value, BindingPriority priority = BindingPriority.LocalValue) { - return new InstancedBinding(value, BindingMode.OneTime, priority); + return new InstancedBinding(ObservableEx.SingleValue(value), BindingMode.OneTime, priority); } /// @@ -106,7 +107,7 @@ namespace Avalonia.Data { _ = observer ?? throw new ArgumentNullException(nameof(observer)); - return new InstancedBinding(observer, BindingMode.OneWayToSource, priority); + return new InstancedBinding((IObservable)observer, BindingMode.OneWayToSource, priority); } /// @@ -135,7 +136,7 @@ namespace Avalonia.Data /// An instance. public InstancedBinding WithPriority(BindingPriority priority) { - return new InstancedBinding(Value, Mode, priority); + return new InstancedBinding(Source, Mode, priority); } } } diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index b7b44a7dfe..093597c6a0 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -109,7 +109,7 @@ namespace Avalonia.Styling if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) { - return new PropertySetterBindingInstance(target, instance, Property, mode, i.Observable!); + return new PropertySetterBindingInstance(target, instance, Property, mode, i.Source); } throw new NotSupportedException(); diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index e859a6e725..8f532b9803 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -7,6 +7,7 @@ using Avalonia.Data; using System; using Avalonia.Controls.Utils; using Avalonia.Markup.Xaml.MarkupExtensions; +using Avalonia.Reactive; namespace Avalonia.Controls { @@ -111,9 +112,9 @@ namespace Avalonia.Controls if (result != null) { - if(result.Subject != null) + if(result.Source is IAvaloniaSubject subject) { - var bindingHelper = new CellEditBinding(result.Subject); + var bindingHelper = new CellEditBinding(subject); var instanceBinding = new InstancedBinding(bindingHelper.InternalSubject, result.Mode, result.Priority); BindingOperations.Apply(target, property, instanceBinding, null); diff --git a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs index 1515ff2c90..993f63b4d3 100644 --- a/src/Markup/Avalonia.Markup/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup/Data/MultiBinding.cs @@ -85,8 +85,8 @@ namespace Avalonia.Data var children = Bindings.Select(x => x.Initiate(target, null)); - var input = children.Select(x => x?.Observable!) - .Where(x => x is not null) + var input = children.Select(x => x?.Source) + .Where(x => x is not null)! .CombineLatest() .Select(x => ConvertValue(x, targetType, converter)) .Where(x => x != BindingOperations.DoNothing); diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs index 656c2cbbbc..3ba8e8354d 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs @@ -334,7 +334,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Foo", }; - var result = binding.Initiate(target, TextBox.TextProperty).Value; + var result = binding.Initiate(target, TextBox.TextProperty).Source; Assert.IsType(((BindingExpression)result).Converter); } @@ -350,7 +350,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Foo", }; - var result = binding.Initiate(target, TextBox.TextProperty).Value; + var result = binding.Initiate(target, TextBox.TextProperty).Source; Assert.Same(converter.Object, ((BindingExpression)result).Converter); } @@ -367,7 +367,7 @@ namespace Avalonia.Markup.UnitTests.Data Path = "Bar", }; - var result = binding.Initiate(target, TextBox.TextProperty).Value; + var result = binding.Initiate(target, TextBox.TextProperty).Source; Assert.Same("foo", ((BindingExpression)result).ConverterParameter); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs index 2a0750b131..680c49d098 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_Converters.cs @@ -24,7 +24,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.TextProperty).Observable; + TextBlock.TextProperty).Source; Assert.Same(StringConverters.IsNullOrEmpty, expressionObserver.Converter); } @@ -46,7 +46,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.TextProperty).Observable; + TextBlock.TextProperty).Source; Assert.IsType(expressionObserver.Converter); } @@ -69,7 +69,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.TagProperty).Observable; + TextBlock.TagProperty).Source; Assert.IsType(expressionObserver.Converter); } @@ -92,7 +92,7 @@ namespace Avalonia.Markup.UnitTests.Data var expressionObserver = (BindingExpression)target.Initiate( textBlock, - TextBlock.MarginProperty).Observable; + TextBlock.MarginProperty).Source; Assert.Same(DefaultValueConverter.Instance, expressionObserver.Converter); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs index 45deb97f51..505eddb146 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_DataValidation.cs @@ -20,7 +20,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)); var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: false); - var subject = (BindingExpression)instanced.Value; + var subject = (BindingExpression)instanced.Source; object result = null; subject.Subscribe(x => result = x); @@ -38,7 +38,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)); var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); - var subject = (BindingExpression)instanced.Value; + var subject = (BindingExpression)instanced.Source; object result = null; subject.Subscribe(x => result = x); @@ -56,7 +56,7 @@ namespace Avalonia.Markup.UnitTests.Data var target = new Binding(nameof(Class1.Foo)) { Priority = BindingPriority.Template }; var instanced = target.Initiate(textBlock, TextBlock.TextProperty, enableDataValidation: true); - var subject = (BindingExpression)instanced.Value; + var subject = (BindingExpression)instanced.Source; object result = null; subject.Subscribe(x => result = x); diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs index a7ef2c4e4d..bf9631760a 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs @@ -30,7 +30,7 @@ namespace Avalonia.Markup.UnitTests.Data }; var target = new Control { DataContext = source }; - var observable = binding.Initiate(target, null).Observable; + var observable = binding.Initiate(target, null).Source; var result = await observable.Take(1); Assert.Equal("1,2,3", result); @@ -59,7 +59,7 @@ namespace Avalonia.Markup.UnitTests.Data }; var target = new Control { DataContext = source }; - var observable = binding.Initiate(target, null).Observable; + var observable = binding.Initiate(target, null).Source; var result = await observable.Take(1); Assert.Equal("1,2,3", result); From d6a68c8af838b831ba595e22090578c8bbad91c6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 17 Jan 2023 18:34:55 +0100 Subject: [PATCH 04/23] Expose binding plugins in a different API. --- .../Data/Core/Plugins/BindingPlugins.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs diff --git a/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs new file mode 100644 index 0000000000..6d88d55774 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/Plugins/BindingPlugins.cs @@ -0,0 +1,28 @@ +using System.Collections.Generic; + +namespace Avalonia.Data.Core.Plugins +{ + /// + /// Holds a registry of plugins used for bindings. + /// + public static class BindingPlugins + { + /// + /// An ordered collection of property accessor plugins that can be used to customize + /// the reading and subscription of property values on a type. + /// + public static IList PropertyAccessors => ExpressionObserver.PropertyAccessors; + + /// + /// An ordered collection of validation checker plugins that can be used to customize + /// the validation of view model and model data. + /// + public static IList DataValidators => ExpressionObserver.DataValidators; + + /// + /// An ordered collection of stream plugins that can be used to customize the behavior + /// of the '^' stream binding operator. + /// + public static IList StreamHandlers => ExpressionObserver.StreamHandlers; + } +} From 43e00b710bc55e7824a7e74661088b4d0f6cc052 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 18 Jan 2023 11:36:33 +0100 Subject: [PATCH 05/23] Make concrete binding plugin classes internal. --- .../Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs | 2 +- .../Data/Core/Plugins/DataAnnotationsValidationPlugin.cs | 2 +- .../Data/Core/Plugins/ExceptionValidationPlugin.cs | 2 +- src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs | 2 +- .../Data/Core/Plugins/InpcPropertyAccessorPlugin.cs | 2 +- src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs | 2 +- src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs | 2 +- src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs index 34f8e568d4..f111d8917b 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs @@ -8,7 +8,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Reads a property from a . /// - public class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin + internal class AvaloniaPropertyAccessorPlugin : IPropertyAccessorPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.PropertyAccessorsRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs index bc300386b9..ba5f59ea23 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/DataAnnotationsValidationPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on that have s. /// - public class DataAnnotationsValidationPlugin : IDataValidationPlugin + internal class DataAnnotationsValidationPlugin : IDataValidationPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs index 2bb8da2c74..e60a341309 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ExceptionValidationPlugin.cs @@ -7,7 +7,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties that report errors by throwing exceptions. /// - public class ExceptionValidationPlugin : IDataValidationPlugin + internal class ExceptionValidationPlugin : IDataValidationPlugin { /// [RequiresUnreferencedCode(TrimmingMessages.DataValidationPluginRequiresUnreferencedCodeMessage)] diff --git a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs index 87a2f67ee8..3384a99333 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/IndeiValidationPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// /// Validates properties on objects that implement . /// - public class IndeiValidationPlugin : IDataValidationPlugin + internal class IndeiValidationPlugin : IDataValidationPlugin { private static readonly WeakEvent ErrorsChangedWeakEvent = WeakEvent.Register( diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs index 5b19e995cc..7c2caf02b4 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs @@ -11,7 +11,7 @@ namespace Avalonia.Data.Core.Plugins /// Reads a property from a standard C# object that optionally supports the /// interface. /// - public class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin + internal class InpcPropertyAccessorPlugin : IPropertyAccessorPlugin { private readonly Dictionary<(Type, string), PropertyInfo?> _propertyLookup = new Dictionary<(Type, string), PropertyInfo?>(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs index 2397ce483d..8170edd653 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs @@ -6,7 +6,7 @@ using System.Reflection; namespace Avalonia.Data.Core.Plugins { - public class MethodAccessorPlugin : IPropertyAccessorPlugin + internal class MethodAccessorPlugin : IPropertyAccessorPlugin { private readonly Dictionary<(Type, string), MethodInfo?> _methodLookup = new Dictionary<(Type, string), MethodInfo?>(); diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index b40628fd35..2b9da0a61a 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// Handles binding to s for the '^' stream binding operator. /// [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] - public class ObservableStreamPlugin : IStreamPlugin + internal class ObservableStreamPlugin : IStreamPlugin { private static MethodInfo? s_observableGeneric; private static MethodInfo? s_observableSelect; diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index 715f4604cf..42a050778e 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -10,7 +10,7 @@ namespace Avalonia.Data.Core.Plugins /// Handles binding to s for the '^' stream binding operator. /// [UnconditionalSuppressMessage("Trimming", "IL3050", Justification = TrimmingMessages.IgnoreNativeAotSupressWarningMessage)] - public class TaskStreamPlugin : IStreamPlugin + internal class TaskStreamPlugin : IStreamPlugin { /// /// Checks whether this plugin handles the specified value. From 658fd804af0835600d6c5e0fcdfa2826590f4ace Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 01:36:45 +0100 Subject: [PATCH 06/23] Removed some temporary List from text layout --- .../Media/TextFormatting/SplitResult.cs | 11 ++++ .../Media/TextFormatting/TextCharacters.cs | 9 +-- .../TextCollapsingProperties.cs | 6 +- .../TextFormatting/TextEllipsisHelper.cs | 62 ++++++++++--------- .../Media/TextFormatting/TextFormatterImpl.cs | 53 +++++++++------- .../Media/TextFormatting/TextLayout.cs | 51 ++++++++------- .../TextLeadingPrefixCharacterEllipsis.cs | 50 ++++++++------- .../Media/TextFormatting/TextLineImpl.cs | 56 ++++++++--------- .../TextTrailingCharacterEllipsis.cs | 7 +-- .../TextTrailingWordEllipsis.cs | 8 +-- 10 files changed, 169 insertions(+), 144 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs index 53021c4656..c1ac57ce46 100644 --- a/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs +++ b/src/Avalonia.Base/Media/TextFormatting/SplitResult.cs @@ -26,5 +26,16 @@ /// The second part. /// public T? Second { get; } + + /// + /// Deconstructs the split results into its components. + /// + /// On return, contains the first part. + /// On return, contains the second part. + public void Deconstruct(out T first, out T? second) + { + first = First; + second = Second; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 2525f0dbf9..6454f9bfa3 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -46,24 +46,21 @@ namespace Avalonia.Media.TextFormatting /// Gets a list of . /// /// The shapeable text characters. - internal IReadOnlyList GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, - ref TextRunProperties? previousProperties) + internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, + ref TextRunProperties? previousProperties, List results) { - var shapeableCharacters = new List(2); var properties = Properties; while (!text.IsEmpty) { var shapeableRun = CreateShapeableRun(text, properties, biDiLevel, ref previousProperties); - shapeableCharacters.Add(shapeableRun); + results.Add(shapeableRun); text = text.Slice(shapeableRun.Length); previousProperties = shapeableRun.Properties; } - - return shapeableCharacters; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs index 01804e1ce3..72882df0b5 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCollapsingProperties.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// Properties of text collapsing. @@ -21,6 +19,6 @@ namespace Avalonia.Media.TextFormatting /// Collapses given text line. /// /// Text line to collapse. - public abstract List? Collapse(TextLine textLine); + public abstract TextRun[]? Collapse(TextLine textLine); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 528cd45581..97f8b2483b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,15 +1,18 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Linq; using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { internal static class TextEllipsisHelper { - public static List? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) + public static TextRun[]? Collapse(TextLine textLine, TextCollapsingProperties properties, bool isWordEllipsis) { var textRuns = textLine.TextRuns; - if (textRuns == null || textRuns.Count == 0) + if (textRuns.Count == 0) { return null; } @@ -22,7 +25,7 @@ namespace Avalonia.Media.TextFormatting if (properties.Width < shapedSymbol.GlyphRun.Size.Width) { //Not enough space to fit in the symbol - return new List(0); + return Array.Empty(); } var availableWidth = properties.Width - shapedSymbol.Size.Width; @@ -70,18 +73,7 @@ namespace Avalonia.Media.TextFormatting collapsedLength += measuredLength; - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); - - collapsedRuns.AddRange(splitResult.First); - } - - collapsedRuns.Add(shapedSymbol); - - return collapsedRuns; + return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); } availableWidth -= shapedRun.Size.Width; @@ -94,18 +86,7 @@ namespace Avalonia.Media.TextFormatting //The whole run needs to fit into available space if (currentWidth + drawableRun.Size.Width > availableWidth) { - var collapsedRuns = new List(textRuns.Count); - - if (collapsedLength > 0) - { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength); - - collapsedRuns.AddRange(splitResult.First); - } - - collapsedRuns.Add(shapedSymbol); - - return collapsedRuns; + return CreateCollapsedRuns(textRuns, collapsedLength, shapedSymbol); } availableWidth -= drawableRun.Size.Width; @@ -121,5 +102,30 @@ namespace Avalonia.Media.TextFormatting return null; } + + private static TextRun[] CreateCollapsedRuns(IReadOnlyList textRuns, int collapsedLength, + TextRun shapedSymbol) + { + if (collapsedLength <= 0) + { + return new[] { shapedSymbol }; + } + + // perf note: the runs are very likely to come from TextLineImpl + // which already uses an array: ToArray() won't ever be called in this case + var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); + + var (preSplitRuns, _) = TextFormatterImpl.SplitTextRuns(textRunArray, collapsedLength); + + var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; + + for (var i = 0; i < preSplitRuns.Count; ++i) + { + collapsedRuns[i] = preSplitRuns[i]; + } + + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; + return collapsedRuns; + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 8afecb09e2..5c073452f4 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Collections.Generic; +using System.Linq; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; @@ -23,10 +24,10 @@ namespace Avalonia.Media.TextFormatting var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, out var textEndOfLine, out var textSourceLength); - if (previousLineBreak?.RemainingRuns != null) + if (previousLineBreak?.RemainingRuns is { } remainingRuns) { resolvedFlowDirection = previousLineBreak.FlowDirection; - textRuns = previousLineBreak.RemainingRuns; + textRuns = remainingRuns; nextLineBreak = previousLineBreak; } else @@ -45,7 +46,7 @@ namespace Avalonia.Media.TextFormatting { case TextWrapping.NoWrap: { - textLine = new TextLineImpl(textRuns, firstTextSourceIndex, textSourceLength, + textLine = new TextLineImpl(textRuns.ToArray(), firstTextSourceIndex, textSourceLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); textLine.FinalizeLine(); @@ -160,6 +161,14 @@ namespace Avalonia.Media.TextFormatting { var flowDirection = paragraphProperties.FlowDirection; var shapedRuns = new List(); + + if (textRuns.Count == 0) + { + resolvedFlowDirection = flowDirection; + return shapedRuns; + } + + using var biDiData = new BidiData((sbyte)flowDirection); foreach (var textRun in textRuns) @@ -224,7 +233,7 @@ namespace Avalonia.Media.TextFormatting shapeableRun.BidiLevel, currentRun.Properties.CultureInfo, paragraphProperties.DefaultIncrementalTab, paragraphProperties.LetterSpacing); - shapedRuns.AddRange(ShapeTogether(groupedRuns, text, shaperOptions)); + ShapeTogether(groupedRuns, text, shaperOptions, shapedRuns); break; } @@ -309,11 +318,9 @@ namespace Avalonia.Media.TextFormatting && x.Typeface == y.Typeface && x.BaselineAlignment == y.BaselineAlignment; - private static IReadOnlyList ShapeTogether( - IReadOnlyList textRuns, ReadOnlyMemory text, TextShaperOptions options) + private static void ShapeTogether(IReadOnlyList textRuns, ReadOnlyMemory text, + TextShaperOptions options, List results) { - var shapedRuns = new List(textRuns.Count); - var shapedBuffer = TextShaper.Current.ShapeText(text, options); for (var i = 0; i < textRuns.Count; i++) @@ -322,12 +329,10 @@ namespace Avalonia.Media.TextFormatting var splitResult = shapedBuffer.Split(currentRun.Length); - shapedRuns.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); + results.Add(new ShapedTextRun(splitResult.First, currentRun.Properties)); shapedBuffer = splitResult.Second!; } - - return shapedRuns; } /// @@ -335,7 +340,7 @@ namespace Avalonia.Media.TextFormatting /// /// The text characters to form from. /// The bidi levels. - /// + /// A list that will be filled with the processed runs. /// private static void CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels, List processedRuns) @@ -385,7 +390,8 @@ namespace Avalonia.Media.TextFormatting if (j == runTextSpan.Length) { - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, + processedRuns); runLevel = levels[levelIndex]; @@ -398,7 +404,8 @@ namespace Avalonia.Media.TextFormatting } // End of this run - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText.Slice(0, j), runLevel, ref previousProperties, + processedRuns); runText = runText.Slice(j); runTextSpan = runText.Span; @@ -415,7 +422,7 @@ namespace Avalonia.Media.TextFormatting return; } - processedRuns.AddRange(currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties)); + currentRun.GetShapeableCharacters(runText, runLevel, ref previousProperties, processedRuns); } /// @@ -423,8 +430,8 @@ namespace Avalonia.Media.TextFormatting /// /// The text source. /// The first text source index. - /// - /// + /// On return, the end of line, if any. + /// On return, the processed text source length. /// /// The formatted text runs. /// @@ -602,7 +609,7 @@ namespace Avalonia.Media.TextFormatting var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); - var textRuns = new List { new ShapedTextRun(shapedBuffer, properties) }; + var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) }; return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); } @@ -749,12 +756,10 @@ namespace Avalonia.Media.TextFormatting break; } - var splitResult = SplitTextRuns(textRuns, measuredLength); - - var remainingCharacters = splitResult.Second; + var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength); - var lineBreak = remainingCharacters?.Count > 0 ? - new TextLineBreak(null, resolvedFlowDirection, remainingCharacters) : + var lineBreak = postSplitRuns?.Count > 0 ? + new TextLineBreak(null, resolvedFlowDirection, postSplitRuns) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) @@ -762,7 +767,7 @@ namespace Avalonia.Media.TextFormatting lineBreak = new TextLineBreak(currentLineBreak.TextEndOfLine, resolvedFlowDirection); } - var textLine = new TextLineImpl(splitResult.First, firstTextSourceIndex, measuredLength, + var textLine = new TextLineImpl(preSplitRuns.ToArray(), firstTextSourceIndex, measuredLength, paragraphWidth, paragraphProperties, resolvedFlowDirection, lineBreak); diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 468623b356..55b6f14267 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting @@ -13,6 +12,7 @@ namespace Avalonia.Media.TextFormatting private readonly ITextSource _textSource; private readonly TextParagraphProperties _paragraphProperties; private readonly TextTrimming _textTrimming; + private readonly TextLine[] _textLines; private int _textSourceLength; @@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting MaxLines = maxLines; - TextLines = CreateTextLines(); + _textLines = CreateTextLines(); } /// @@ -109,7 +109,7 @@ namespace Avalonia.Media.TextFormatting MaxLines = maxLines; - TextLines = CreateTextLines(); + _textLines = CreateTextLines(); } /// @@ -147,7 +147,8 @@ namespace Avalonia.Media.TextFormatting /// /// The text lines. /// - public IReadOnlyList TextLines { get; private set; } + public IReadOnlyList TextLines + => _textLines; /// /// Gets the bounds of the layout. @@ -164,14 +165,14 @@ namespace Avalonia.Media.TextFormatting /// The origin. public void Draw(DrawingContext context, Point origin) { - if (!TextLines.Any()) + if (_textLines.Length == 0) { return; } var (currentX, currentY) = origin; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { textLine.Draw(context, new Point(currentX + textLine.Start, currentY)); @@ -186,7 +187,7 @@ namespace Avalonia.Media.TextFormatting /// public Rect HitTestTextPosition(int textPosition) { - if (TextLines.Count == 0) + if (_textLines.Length == 0) { return new Rect(); } @@ -198,7 +199,7 @@ namespace Avalonia.Media.TextFormatting var currentY = 0.0; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { var end = textLine.FirstTextSourceIndex + textLine.Length; @@ -230,11 +231,11 @@ namespace Avalonia.Media.TextFormatting return Array.Empty(); } - var result = new List(TextLines.Count); + var result = new List(_textLines.Length); var currentY = 0d; - foreach (var textLine in TextLines) + foreach (var textLine in _textLines) { //Current line isn't covered. if (textLine.FirstTextSourceIndex + textLine.Length < start) @@ -284,13 +285,12 @@ namespace Avalonia.Media.TextFormatting { var currentY = 0d; - var lineIndex = 0; TextLine? currentLine = null; CharacterHit characterHit; - for (; lineIndex < TextLines.Count; lineIndex++) + for (var lineIndex = 0; lineIndex < _textLines.Length; lineIndex++) { - currentLine = TextLines[lineIndex]; + currentLine = _textLines[lineIndex]; if (currentY + currentLine.Height > point.Y) { @@ -322,12 +322,12 @@ namespace Avalonia.Media.TextFormatting if (charIndex > _textSourceLength) { - return TextLines.Count - 1; + return _textLines.Length - 1; } - for (var index = 0; index < TextLines.Count; index++) + for (var index = 0; index < _textLines.Length; index++) { - var textLine = TextLines[index]; + var textLine = _textLines[index]; if (textLine.FirstTextSourceIndex + textLine.Length < charIndex) { @@ -341,7 +341,7 @@ namespace Avalonia.Media.TextFormatting } } - return TextLines.Count - 1; + return _textLines.Length - 1; } private TextHitTestResult GetHitTestResult(TextLine textLine, CharacterHit characterHit, Point point) @@ -424,7 +424,7 @@ namespace Avalonia.Media.TextFormatting height += textLine.Height; } - private IReadOnlyList CreateTextLines() + private TextLine[] CreateTextLines() { if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { @@ -432,7 +432,7 @@ namespace Avalonia.Media.TextFormatting Bounds = new Rect(0, 0, 0, textLine.Height); - return new List { textLine }; + return new TextLine[] { textLine }; } var textLines = new List(); @@ -443,12 +443,14 @@ namespace Avalonia.Media.TextFormatting TextLine? previousLine = null; + var textFormatter = TextFormatter.Current; + while (true) { - var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, + var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); - if(textLine == null || textLine.Length == 0) + if (textLine.Length == 0) { if (previousLine != null && previousLine.NewLineLength > 0) { @@ -524,8 +526,9 @@ namespace Avalonia.Media.TextFormatting { var whitespaceWidth = 0d; - foreach (var line in textLines) + for (var i = 0; i < textLines.Count; i++) { + var line = textLines[i]; var lineWhitespaceWidth = line.Width - line.WidthIncludingTrailingWhitespace; if (lineWhitespaceWidth > whitespaceWidth) @@ -549,7 +552,7 @@ namespace Avalonia.Media.TextFormatting } } - return textLines; + return textLines.ToArray(); } /// @@ -569,7 +572,7 @@ namespace Avalonia.Media.TextFormatting public void Dispose() { - foreach (var line in TextLines) + foreach (var line in _textLines) { line.Dispose(); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index e30a0fe9f4..672a15b398 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -1,5 +1,7 @@ -using System; +// ReSharper disable ForCanBeConvertedToForeach +using System; using System.Collections.Generic; +using System.Linq; namespace Avalonia.Media.TextFormatting { @@ -39,11 +41,12 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { var textRuns = textLine.TextRuns; - if (textRuns == null || textRuns.Count == 0) + if (textRuns.Count == 0) { return null; } @@ -54,7 +57,7 @@ namespace Avalonia.Media.TextFormatting if (Width < shapedSymbol.GlyphRun.Size.Width) { - return new List(0); + return Array.Empty(); } // Overview of ellipsis structure @@ -75,41 +78,48 @@ namespace Avalonia.Media.TextFormatting { shapedRun.TryMeasureCharacters(availableWidth, out var measuredLength); - var collapsedRuns = new List(textRuns.Count); - if (measuredLength > 0) { - IReadOnlyList? preSplitRuns = null; + var collapsedRuns = new List(textRuns.Count + 1); + + // perf note: the runs are very likely to come from TextLineImpl, + // which already uses an array: ToArray() won't ever be called in this case + var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); + + IReadOnlyList? preSplitRuns; IReadOnlyList? postSplitRuns; if (_prefixLength > 0) { - var splitResult = TextFormatterImpl.SplitTextRuns(textRuns, - Math.Min(_prefixLength, measuredLength)); + (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns( + textRunArray, Math.Min(_prefixLength, measuredLength)); - collapsedRuns.AddRange(splitResult.First); - - preSplitRuns = splitResult.First; - postSplitRuns = splitResult.Second; + for (var i = 0; i < preSplitRuns.Count; i++) + { + var preSplitRun = preSplitRuns[i]; + collapsedRuns.Add(preSplitRun); + } } else { - postSplitRuns = textRuns; + preSplitRuns = null; + postSplitRuns = textRunArray; } collapsedRuns.Add(shapedSymbol); if (measuredLength <= _prefixLength || postSplitRuns is null) { - return collapsedRuns; + return collapsedRuns.ToArray(); } var availableSuffixWidth = availableWidth; if (preSplitRuns is not null) { - foreach (var run in preSplitRuns) + for (var i = 0; i < preSplitRuns.Count; i++) { + var run = preSplitRuns[i]; if (run is DrawableTextRun drawableTextRun) { availableSuffixWidth -= drawableTextRun.Size.Width; @@ -143,13 +153,11 @@ namespace Avalonia.Media.TextFormatting } } } - } - else - { - collapsedRuns.Add(shapedSymbol); + + return collapsedRuns.ToArray(); } - return collapsedRuns; + return new TextRun[] { shapedSymbol }; } availableWidth -= shapedRun.Size.Width; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ab9686a34a..ae6df3a232 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,19 +1,18 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - internal class TextLineImpl : TextLine + internal sealed class TextLineImpl : TextLine { - private IReadOnlyList _textRuns; + private TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; private readonly FlowDirection _resolvedFlowDirection; - public TextLineImpl(IReadOnlyList textRuns, int firstTextSourceIndex, int length, double paragraphWidth, + public TextLineImpl(TextRun[] textRuns, int firstTextSourceIndex, int length, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection = FlowDirection.LeftToRight, TextLineBreak? lineBreak = null, bool hasCollapsed = false) { @@ -147,7 +146,7 @@ namespace Avalonia.Media.TextFormatting var collapsedLine = new TextLineImpl(collapsedRuns, FirstTextSourceIndex, Length, _paragraphWidth, _paragraphProperties, _resolvedFlowDirection, TextLineBreak, true); - if (collapsedRuns.Count > 0) + if (collapsedRuns.Length > 0) { collapsedLine.FinalizeLine(); } @@ -166,7 +165,7 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetCharacterHitFromDistance(double distance) { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return new CharacterHit(); } @@ -182,7 +181,7 @@ namespace Avalonia.Media.TextFormatting if (distance >= WidthIncludingTrailingWhitespace) { - var lastRun = _textRuns[_textRuns.Count - 1]; + var lastRun = _textRuns[_textRuns.Length - 1]; var size = 0.0; @@ -199,7 +198,7 @@ namespace Avalonia.Media.TextFormatting var currentPosition = FirstTextSourceIndex; var currentDistance = 0.0; - for (var i = 0; i < _textRuns.Count; i++) + for (var i = 0; i < _textRuns.Length; i++) { var currentRun = _textRuns[i]; @@ -208,7 +207,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftIndex = i; currentPosition += currentRun.Length; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1) + while (rightToLeftIndex + 1 <= _textRuns.Length - 1) { var nextShaped = _textRuns[++rightToLeftIndex] as ShapedTextRun; @@ -224,7 +223,7 @@ namespace Avalonia.Media.TextFormatting for (var j = i; i <= rightToLeftIndex; j++) { - if (j > _textRuns.Count - 1) + if (j > _textRuns.Length - 1) { break; } @@ -254,7 +253,7 @@ namespace Avalonia.Media.TextFormatting if (currentRun is DrawableTextRun drawableTextRun) { - if (i < _textRuns.Count - 1 && currentDistance + drawableTextRun.Size.Width < distance) + if (i < _textRuns.Length - 1 && currentDistance + drawableTextRun.Size.Width < distance) { currentDistance += drawableTextRun.Size.Width; @@ -328,7 +327,7 @@ namespace Avalonia.Media.TextFormatting if (flowDirection == FlowDirection.LeftToRight) { - for (var index = 0; index < _textRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { var currentRun = _textRuns[index]; @@ -338,7 +337,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftWidth = shapedRun.Size.Width; - while (i + 1 <= _textRuns.Count - 1) + while (i + 1 <= _textRuns.Length - 1) { var nextRun = _textRuns[i + 1]; @@ -402,7 +401,7 @@ namespace Avalonia.Media.TextFormatting { currentDistance += WidthIncludingTrailingWhitespace; - for (var index = _textRuns.Count - 1; index >= 0; index--) + for (var index = _textRuns.Length - 1; index >= 0; index--) { var currentRun = _textRuns[index]; @@ -502,7 +501,7 @@ namespace Avalonia.Media.TextFormatting /// public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return new CharacterHit(); } @@ -637,7 +636,7 @@ namespace Avalonia.Media.TextFormatting var rightToLeftIndex = index; var rightToLeftWidth = currentShapedRun.Size.Width; - while (rightToLeftIndex + 1 <= _textRuns.Count - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) + while (rightToLeftIndex + 1 <= _textRuns.Length - 1 && _textRuns[rightToLeftIndex + 1] is ShapedTextRun nextShapedRun) { if (nextShapedRun == null || nextShapedRun.ShapedBuffer.IsLeftToRight) { @@ -981,7 +980,7 @@ namespace Avalonia.Media.TextFormatting public override void Dispose() { - for (int i = 0; i < _textRuns.Count; i++) + for (int i = 0; i < _textRuns.Length; i++) { if (_textRuns[i] is ShapedTextRun shapedTextRun) { @@ -1013,7 +1012,7 @@ namespace Avalonia.Media.TextFormatting private void BidiReorder() { - if (_textRuns.Count == 0) + if (_textRuns.Length == 0) { return; } @@ -1025,7 +1024,7 @@ namespace Avalonia.Media.TextFormatting var current = orderedRun; - for (var i = 1; i < _textRuns.Count; i++) + for (var i = 1; i < _textRuns.Length; i++) { run = _textRuns[i]; @@ -1044,7 +1043,7 @@ namespace Avalonia.Media.TextFormatting sbyte max = 0; var min = sbyte.MaxValue; - for (var i = 0; i < _textRuns.Count; i++) + for (var i = 0; i < _textRuns.Length; i++) { var currentRun = _textRuns[i]; @@ -1095,13 +1094,14 @@ namespace Avalonia.Media.TextFormatting minLevelToReverse--; } - var textRuns = new List(_textRuns.Count); + var textRuns = new TextRun[_textRuns.Length]; + var index = 0; current = orderedRun; while (current != null) { - textRuns.Add(current.Run); + textRuns[index++] = current.Run; current = current.Next; } @@ -1197,7 +1197,7 @@ namespace Avalonia.Media.TextFormatting var runIndex = GetRunIndexAtCharacterIndex(codepointIndex, LogicalDirection.Forward, out var currentPosition); - while (runIndex < _textRuns.Count) + while (runIndex < _textRuns.Length) { var currentRun = _textRuns[runIndex]; @@ -1346,7 +1346,7 @@ namespace Avalonia.Media.TextFormatting textPosition = FirstTextSourceIndex; TextRun? previousRun = null; - while (runIndex < _textRuns.Count) + while (runIndex < _textRuns.Length) { var currentRun = _textRuns[runIndex]; @@ -1395,7 +1395,7 @@ namespace Avalonia.Media.TextFormatting } } - if (runIndex + 1 >= _textRuns.Count) + if (runIndex + 1 >= _textRuns.Length) { return runIndex; } @@ -1411,7 +1411,7 @@ namespace Avalonia.Media.TextFormatting return runIndex; } - if (runIndex + 1 >= _textRuns.Count) + if (runIndex + 1 >= _textRuns.Length) { return runIndex; } @@ -1448,14 +1448,14 @@ namespace Avalonia.Media.TextFormatting var lineHeight = _paragraphProperties.LineHeight; - var lastRunIndex = _textRuns.Count - 1; + var lastRunIndex = _textRuns.Length - 1; if (lastRunIndex > 0 && _textRuns[lastRunIndex] is TextEndOfLine) { lastRunIndex--; } - for (var index = 0; index < _textRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { switch (_textRuns[index]) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs index deecbbe476..ccae99cc75 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -1,6 +1,4 @@ -using System.Collections.Generic; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// A collapsing properties to collapse whole line toward the end @@ -26,7 +24,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, false); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs index c291e1dfb9..c622c76a60 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -1,7 +1,4 @@ -using System.Collections.Generic; -using Avalonia.Utilities; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// a collapsing properties to collapse whole line toward the end @@ -31,7 +28,8 @@ namespace Avalonia.Media.TextFormatting /// public override TextRun Symbol { get; } - public override List? Collapse(TextLine textLine) + /// + public override TextRun[]? Collapse(TextLine textLine) { return TextEllipsisHelper.Collapse(textLine, this, true); } From 96b423900f734c698932534dacefd59827557470 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 16 Jan 2023 11:50:14 +0100 Subject: [PATCH 07/23] TextRunProperties: don't allocate if the typeface hasn't changed --- src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs index 86b701cb4b..7bad99f33f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextRunProperties.cs @@ -93,6 +93,9 @@ namespace Avalonia.Media.TextFormatting internal TextRunProperties WithTypeface(Typeface typeface) { + if (this is GenericTextRunProperties other && other.Typeface == typeface) + return this; + return new GenericTextRunProperties(typeface, FontRenderingEmSize, TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment); } From 076d10fcaf6f14d14585b147ffdd72f578efaa7e Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 16 Jan 2023 14:57:06 +0100 Subject: [PATCH 08/23] BiDiAlgorithm and BiDiData instances are reusable --- .../Media/TextFormatting/ShapedBuffer.cs | 43 +++++++---------- .../Media/TextFormatting/TextFormatterImpl.cs | 25 ++++++---- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 48 ++++++++----------- .../Media/TextFormatting/Unicode/BiDiData.cs | 38 +++++++++------ src/Avalonia.Base/Utilities/ArrayBuilder.cs | 18 ++----- src/Avalonia.Base/Utilities/ArraySlice.cs | 10 +--- .../TextBoxTextInputMethodClient.cs | 2 +- .../Media/TextFormatting/BiDiClassTests.cs | 6 +-- 8 files changed, 84 insertions(+), 106 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index b05fab08fa..41bba2cd09 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -8,17 +8,17 @@ namespace Avalonia.Media.TextFormatting public sealed class ShapedBuffer : IList, IDisposable { private static readonly IComparer s_clusterComparer = new CompareClusters(); - private bool _bufferRented; - - public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) : - this(text, - new ArraySlice(ArrayPool.Shared.Rent(bufferLength), 0, bufferLength), - glyphTypeface, - fontRenderingEmSize, - bidiLevel) + + private GlyphInfo[]? _rentedBuffer; + + public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) { - _bufferRented = true; - Length = bufferLength; + _rentedBuffer = ArrayPool.Shared.Rent(bufferLength); + Text = text; + GlyphInfos = new ArraySlice(_rentedBuffer, 0, bufferLength); + GlyphTypeface = glyphTypeface; + FontRenderingEmSize = fontRenderingEmSize; + BidiLevel = bidiLevel; } internal ShapedBuffer(ReadOnlyMemory text, ArraySlice glyphInfos, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) @@ -28,12 +28,12 @@ namespace Avalonia.Media.TextFormatting GlyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; BidiLevel = bidiLevel; - Length = GlyphInfos.Length; } - internal ArraySlice GlyphInfos { get; } - - public int Length { get; } + internal ArraySlice GlyphInfos { get; private set; } + + public int Length + => GlyphInfos.Length; public IGlyphTypeface GlyphTypeface { get; } @@ -271,18 +271,11 @@ namespace Avalonia.Media.TextFormatting public void Dispose() { - GC.SuppressFinalize(this); - if (_bufferRented) - { - GlyphInfos.ReturnRent(); - } - } - - ~ShapedBuffer() - { - if (_bufferRented) + if (_rentedBuffer is not null) { - GlyphInfos.ReturnRent(); + ArrayPool.Shared.Return(_rentedBuffer); + _rentedBuffer = null; + GlyphInfos = ArraySlice.Empty; // ensure we don't misuse the returned array } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 5c073452f4..7614c8e3dc 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -11,6 +11,10 @@ namespace Avalonia.Media.TextFormatting internal class TextFormatterImpl : TextFormatter { private static readonly char[] s_empty = { ' ' }; + private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength]; + + [ThreadStatic] private static BidiData? t_bidiData; + [ThreadStatic] private static BidiAlgorithm? t_bidiAlgorithm; /// public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, @@ -169,21 +173,24 @@ namespace Avalonia.Media.TextFormatting } - using var biDiData = new BidiData((sbyte)flowDirection); + var biDiData = t_bidiData ??= new BidiData(); + biDiData.Reset(); + biDiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; foreach (var textRun in textRuns) { - if (textRun.Text.IsEmpty) - { - biDiData.Append(new char[textRun.Length]); - } + ReadOnlySpan text; + if (!textRun.Text.IsEmpty) + text = textRun.Text.Span; + else if (textRun.Length == TextRun.DefaultTextSourceLength) + text = s_defaultText; else - { - biDiData.Append(textRun.Text.Span); - } + text = new char[textRun.Length]; + + biDiData.Append(text); } - using var biDi = new BidiAlgorithm(); + var biDi = t_bidiAlgorithm ??= new BidiAlgorithm(); biDi.Process(biDiData); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 100d381afe..e770ba9e91 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -6,7 +6,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; -using Avalonia.Collections.Pooled; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode @@ -28,7 +27,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// as much as possible. /// /// - internal struct BidiAlgorithm : IDisposable + internal sealed class BidiAlgorithm { /// /// The original BiDiClass classes as provided by the caller @@ -67,7 +66,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The forward mapping maps the start index to the end index. /// The reverse mapping maps the end index to the start index. /// - private BidiDictionary? _isolatePairs; + private readonly BidiDictionary _isolatePairs = new(); /// /// The working BiDi classes @@ -98,7 +97,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The status stack used during resolution of explicit /// embedding and isolating runs /// - private readonly Stack _statusStack = new Stack(); + private readonly Stack _statusStack = new(); /// /// Mapping used to virtually remove characters for rule X9 @@ -108,7 +107,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Re-usable list of level runs /// - private readonly List _levelRuns = new List(); + private readonly List _levelRuns = new(); /// /// Mapping for the current isolating sequence, built @@ -119,7 +118,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// A stack of pending isolate openings used by FindIsolatePairs() /// - private Stack? _pendingIsolateOpenings; + private readonly Stack _pendingIsolateOpenings = new(); /// /// The level of the isolating run currently being processed @@ -175,12 +174,12 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Reusable list of pending opening brackets used by the /// LocatePairedBrackets method /// - private readonly List _pendingOpeningBrackets = new List(); + private readonly List _pendingOpeningBrackets = new(); /// /// Resolved list of paired brackets /// - private readonly List _pairedBrackets = new List(); + private readonly List _pairedBrackets = new(); /// /// Initializes a new instance of the class. @@ -228,7 +227,7 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs?.Clear(); + _isolatePairs.Clear(); _workingClassesBuffer.Clear(); _levelRuns.Clear(); _resolvedLevelsBuffer.Clear(); @@ -324,7 +323,7 @@ namespace Avalonia.Media.TextFormatting.Unicode // Skip isolate pairs // (Because we're working with a slice, we need to adjust the indices // we're using for the isolatePairs map) - if (_isolatePairs?.TryGetValue(data.Start + i, out i) == true) + if (_isolatePairs.TryGetValue(data.Start + i, out i)) { i -= data.Start; } @@ -359,7 +358,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _hasIsolates = false; // BD9... - _pendingIsolateOpenings?.Clear(); + _pendingIsolateOpenings.Clear(); for (var i = 0; i < _originalClasses.Length; i++) { @@ -371,16 +370,14 @@ namespace Avalonia.Media.TextFormatting.Unicode case BidiClass.RightToLeftIsolate: case BidiClass.FirstStrongIsolate: { - _pendingIsolateOpenings ??= new Stack(); _pendingIsolateOpenings.Push(i); _hasIsolates = true; break; } case BidiClass.PopDirectionalIsolate: { - if (_pendingIsolateOpenings?.Count > 0) + if (_pendingIsolateOpenings.Count > 0) { - _isolatePairs ??= new BidiDictionary(); _isolatePairs.Add(_pendingIsolateOpenings.Pop(), i); } @@ -501,7 +498,7 @@ namespace Avalonia.Media.TextFormatting.Unicode if (resolvedIsolate == BidiClass.FirstStrongIsolate) { - if (_isolatePairs == null || !_isolatePairs.TryGetValue(i, out var endOfIsolate)) + if (!_isolatePairs.TryGetValue(i, out var endOfIsolate)) { endOfIsolate = _originalClasses.Length; } @@ -832,7 +829,7 @@ namespace Avalonia.Media.TextFormatting.Unicode var lastCharacterIndex = _isolatedRunMapping[_isolatedRunMapping.Length - 1]; var lastType = _originalClasses[lastCharacterIndex]; if ((lastType == BidiClass.LeftToRightIsolate || lastType == BidiClass.RightToLeftIsolate || lastType == BidiClass.FirstStrongIsolate) && - _isolatePairs?.TryGetValue(lastCharacterIndex, out var nextRunIndex) == true) + _isolatePairs.TryGetValue(lastCharacterIndex, out var nextRunIndex)) { // Find the continuing run index runIndex = FindRunForIndex(nextRunIndex); @@ -855,13 +852,14 @@ namespace Avalonia.Media.TextFormatting.Unicode private void ProcessIsolatedRunSequence(BidiClass sos, BidiClass eos, int runLevel) { // Create mappings onto the underlying data - _runResolvedClasses = new MappedArraySlice(_workingClasses, _isolatedRunMapping.AsSlice()); - _runOriginalClasses = new MappedArraySlice(_originalClasses, _isolatedRunMapping.AsSlice()); - _runLevels = new MappedArraySlice(_resolvedLevels, _isolatedRunMapping.AsSlice()); + var isolatedRunMapping = _isolatedRunMapping.AsSlice(); + _runResolvedClasses = new MappedArraySlice(_workingClasses, isolatedRunMapping); + _runOriginalClasses = new MappedArraySlice(_originalClasses, isolatedRunMapping); + _runLevels = new MappedArraySlice(_resolvedLevels, isolatedRunMapping); if (_hasBrackets) { - _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, _isolatedRunMapping.AsSlice()); - _runPairedBracketValues = new MappedArraySlice(_pairedBracketValues, _isolatedRunMapping.AsSlice()); + _runBiDiPairedBracketTypes = new MappedArraySlice(_pairedBracketTypes, isolatedRunMapping); + _runPairedBracketValues = new MappedArraySlice(_pairedBracketValues, isolatedRunMapping); } _runLevel = runLevel; @@ -1717,13 +1715,5 @@ namespace Avalonia.Media.TextFormatting.Unicode public BidiClass Eos { get; } } - - public void Dispose() - { - _workingClassesBuffer.Dispose(); - _resolvedLevelsBuffer.Dispose(); - _x9Map.Dispose(); - _isolatedRunMapping.Dispose(); - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 0f0b3235e1..106079de8e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -11,7 +11,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Represents a unicode string and all associated attributes /// for each character required for the bidirectional Unicode algorithm /// - internal struct BidiData : IDisposable + internal sealed class BidiData { private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; @@ -20,12 +20,7 @@ namespace Avalonia.Media.TextFormatting.Unicode private ArrayBuilder _savedPairedBracketTypes; private ArrayBuilder _tempLevelBuffer; - public BidiData(sbyte paragraphEmbeddingLevel) - { - ParagraphEmbeddingLevel = paragraphEmbeddingLevel; - } - - public sbyte ParagraphEmbeddingLevel { get; private set; } + public sbyte ParagraphEmbeddingLevel { get; set; } public bool HasBrackets { get; private set; } @@ -36,7 +31,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// Gets the length of the data held by the BidiData /// - public int Length{get; private set; } + public int Length { get; private set; } /// /// Gets the bidi character type of each code point @@ -182,14 +177,27 @@ namespace Avalonia.Media.TextFormatting.Unicode return _tempLevelBuffer.Add(length, false); } - public void Dispose() + /// + /// Resets the bidi data to a clean state. + /// + public void Reset() { - _classes.Dispose(); - _pairedBracketTypes.Dispose(); - _pairedBracketValues.Dispose(); - _savedClasses.Dispose(); - _savedPairedBracketTypes.Dispose(); - _tempLevelBuffer.Dispose(); + _classes.Clear(); + _pairedBracketTypes.Clear(); + _pairedBracketValues.Clear(); + _savedClasses.Clear(); + _savedPairedBracketTypes.Clear(); + _tempLevelBuffer.Clear(); + + ParagraphEmbeddingLevel = 0; + HasBrackets = false; + HasEmbeddings = false; + HasIsolates = false; + Length = 0; + + Classes = default; + PairedBracketTypes = default; + PairedBracketValues = default; } } } diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index e6b67bd383..1c11966a7d 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; -using System.Buffers; using System.Runtime.CompilerServices; namespace Avalonia.Utilities @@ -12,7 +11,7 @@ namespace Avalonia.Utilities /// A helper type for avoiding allocations while building arrays. /// /// The type of item contained in the array. - internal struct ArrayBuilder : IDisposable + internal struct ArrayBuilder where T : struct { private const int DefaultCapacity = 4; @@ -136,7 +135,7 @@ namespace Avalonia.Utilities } // Same expansion algorithm as List. - var newCapacity = length == 0 ? DefaultCapacity : length * 2; + var newCapacity = length == 0 ? DefaultCapacity : (uint)length * 2u; if (newCapacity > MaxCoreClrArrayLength) { @@ -145,15 +144,14 @@ namespace Avalonia.Utilities if (newCapacity < min) { - newCapacity = min; + newCapacity = (uint)min; } - var array = ArrayPool.Shared.Rent(newCapacity); + var array = new T[newCapacity]; if (_size > 0) { Array.Copy(_data!, array, _size); - ArrayPool.Shared.Return(_data!); } _data = array; @@ -182,13 +180,5 @@ namespace Avalonia.Utilities /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length); - - public void Dispose() - { - if (_data != null) - { - ArrayPool.Shared.Return(_data); - } - } } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index b70088a907..3cffef72c5 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -3,7 +3,6 @@ // Ported from: https://github.com/SixLabors/Fonts/ using System; -using System.Buffers; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; @@ -186,13 +185,6 @@ namespace Avalonia.Utilities /// int IReadOnlyCollection.Count => Length; - - public void ReturnRent() - { - if (_data != null) - { - ArrayPool.Shared.Return(_data); - } - } } } + diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs index c1146cceda..10c2f36f43 100644 --- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs +++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs @@ -79,7 +79,7 @@ namespace Avalonia.Controls { if(run.Length > 0) { -#if NET6_0 +#if NET6_0_OR_GREATER builder.Append(run.Text.Span); #else builder.Append(run.Text.Span.ToArray()); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index 9d189d1950..eb69bed1e1 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs @@ -1,8 +1,6 @@ -using System; -using System.Linq; +using System.Linq; using System.Runtime.InteropServices; using System.Text; -using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Xunit; using Xunit.Abstractions; @@ -32,7 +30,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting private bool Run(BiDiClassData t) { var bidi = new BidiAlgorithm(); - var bidiData = new BidiData(t.ParagraphLevel); + var bidiData = new BidiData { ParagraphEmbeddingLevel = t.ParagraphLevel }; var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); From 290c8fe16953b5bb4dd7098752c8b6e693f6ef79 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 17 Jan 2023 03:02:22 +0100 Subject: [PATCH 09/23] Removed most allocations for BidiReorder --- .../Media/TextFormatting/BidiReorderer.cs | 263 ++++++++++++++++++ .../Media/TextFormatting/TextFormatterImpl.cs | 16 +- .../Media/TextFormatting/TextLineImpl.cs | 236 +--------------- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 1 - .../Media/TextFormatting/Unicode/BiDiData.cs | 1 + src/Avalonia.Base/Utilities/ArrayBuilder.cs | 12 +- 6 files changed, 289 insertions(+), 240 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs new file mode 100644 index 0000000000..3fcb7bf420 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -0,0 +1,263 @@ +using System; +using System.Diagnostics; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Reorders text runs according to their bidi level. + /// + /// To avoid allocations, this class is designed to be reused. + internal sealed class BidiReorderer + { + private ArrayBuilder _runs; + private ArrayBuilder _ranges; + + public void BidiReorder(Span textRuns, FlowDirection flowDirection) + { + Debug.Assert(_runs.Length == 0); + Debug.Assert(_ranges.Length == 0); + + if (textRuns.IsEmpty) + { + return; + } + + try + { + _runs.Add(textRuns.Length); + + // Build up the collection of ordered runs. + for (var i = 0; i < textRuns.Length; i++) + { + var textRun = textRuns[i]; + _runs[i] = new OrderedBidiRun(i, textRun, GetRunBidiLevel(textRun, flowDirection)); + + if (i > 0) + { + _runs[i - 1].NextRunIndex = i; + } + } + + // Reorder them into visual order. + var firstIndex = LinearReorder(); + + // Now perform a recursive reversal of each run. + // From the highest level found in the text to the lowest odd level on each line, including intermediate levels + // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. + // https://unicode.org/reports/tr9/#L2 + sbyte max = 0; + var min = sbyte.MaxValue; + + for (var i = 0; i < textRuns.Length; i++) + { + var level = GetRunBidiLevel(textRuns[i], flowDirection); + if (level > max) + { + max = level; + } + + if ((level & 1) != 0 && level < min) + { + min = level; + } + } + + if (min > max) + { + min = max; + } + + if (max == 0 || (min == max && (max & 1) == 0)) + { + // Nothing to reverse. + return; + } + + // Now apply the reversal and replace the original contents. + var minLevelToReverse = max; + int currentIndex; + + while (minLevelToReverse >= min) + { + currentIndex = firstIndex; + + while (currentIndex >= 0) + { + ref var current = ref _runs[currentIndex]; + if (current.Level >= minLevelToReverse && current.Level % 2 != 0) + { + if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters) + { + shapedTextCharacters.Reverse(); + } + } + + currentIndex = current.NextRunIndex; + } + + minLevelToReverse--; + } + + var index = 0; + + currentIndex = firstIndex; + while (currentIndex >= 0) + { + ref var current = ref _runs[currentIndex]; + textRuns[index++] = current.Run; + + currentIndex = current.NextRunIndex; + } + } + finally + { + _runs.Clear(); + _ranges.Clear(); + } + } + + private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) + { + if (run is ShapedTextRun shapedTextRun) + { + return shapedTextRun.BidiLevel; + } + + var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1; + return (sbyte)defaultLevel; + } + + /// + /// Reorders the runs from logical to visual order. + /// + /// + /// The first run index in visual order. + private int LinearReorder() + { + var runIndex = 0; + var rangeIndex = -1; + + while (runIndex >= 0) + { + ref var run = ref _runs[runIndex]; + var nextRunIndex = run.NextRunIndex; + + while (rangeIndex >= 0 + && _ranges[rangeIndex].Level > run.Level + && _ranges[rangeIndex].PreviousRangeIndex >= 0 + && _ranges[_ranges[rangeIndex].PreviousRangeIndex].Level >= run.Level) + { + + rangeIndex = MergeRangeWithPrevious(rangeIndex); + } + + if (rangeIndex >= 0 && _ranges[rangeIndex].Level >= run.Level) + { + // Attach run to the range. + if ((run.Level & 1) != 0) + { + // Odd, range goes to the right of run. + run.NextRunIndex = _ranges[rangeIndex].LeftRunIndex; + _ranges[rangeIndex].LeftRunIndex = runIndex; + } + else + { + // Even, range goes to the left of run. + _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = runIndex; + _ranges[rangeIndex].RightRunIndex = runIndex; + } + + _ranges[rangeIndex].Level = run.Level; + } + else + { + var r = new BidiRange(run.Level, runIndex, runIndex, previousRangeIndex: rangeIndex); + _ranges.AddItem(r); + rangeIndex = _ranges.Length - 1; + } + + runIndex = nextRunIndex; + } + + while (rangeIndex >= 0 && _ranges[rangeIndex].PreviousRangeIndex >= 0) + { + rangeIndex = MergeRangeWithPrevious(rangeIndex); + } + + // Terminate. + _runs[_ranges[rangeIndex].RightRunIndex].NextRunIndex = -1; + + return _runs[_ranges[rangeIndex].LeftRunIndex].RunIndex; + } + + private int MergeRangeWithPrevious(int index) + { + var previousIndex = _ranges[index].PreviousRangeIndex; + ref var previous = ref _ranges[previousIndex]; + + int leftIndex; + int rightIndex; + + if ((previous.Level & 1) != 0) + { + // Odd, previous goes to the right of range. + leftIndex = index; + rightIndex = previousIndex; + } + else + { + // Even, previous goes to the left of range. + leftIndex = previousIndex; + rightIndex = index; + } + + // Stitch them + ref var left = ref _ranges[leftIndex]; + ref var right = ref _ranges[rightIndex]; + _runs[left.RightRunIndex].NextRunIndex = _runs[right.LeftRunIndex].RunIndex; + previous.LeftRunIndex = left.LeftRunIndex; + previous.RightRunIndex = right.RightRunIndex; + + return previousIndex; + } + + private struct OrderedBidiRun + { + public OrderedBidiRun(int runIndex, TextRun run, sbyte level) + { + RunIndex = runIndex; + Run = run; + Level = level; + NextRunIndex = -1; + } + + public int RunIndex { get; } + + public sbyte Level { get; } + + public TextRun Run { get; } + + public int NextRunIndex { get; set; } // -1 if none + } + + private struct BidiRange + { + public BidiRange(sbyte level, int leftRunIndex, int rightRunIndex, int previousRangeIndex) + { + Level = level; + LeftRunIndex = leftRunIndex; + RightRunIndex = rightRunIndex; + PreviousRangeIndex = previousRangeIndex; + } + + public sbyte Level { get; set; } + + public int LeftRunIndex { get; set; } + + public int RightRunIndex { get; set; } + + public int PreviousRangeIndex { get; } // -1 if none + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7614c8e3dc..f3cc0a714e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -173,9 +173,9 @@ namespace Avalonia.Media.TextFormatting } - var biDiData = t_bidiData ??= new BidiData(); - biDiData.Reset(); - biDiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; + var bidiData = t_bidiData ??= new BidiData(); + bidiData.Reset(); + bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; foreach (var textRun in textRuns) { @@ -187,21 +187,21 @@ namespace Avalonia.Media.TextFormatting else text = new char[textRun.Length]; - biDiData.Append(text); + bidiData.Append(text); } - var biDi = t_bidiAlgorithm ??= new BidiAlgorithm(); + var bidiAlgorithm = t_bidiAlgorithm ??= new BidiAlgorithm(); - biDi.Process(biDiData); + bidiAlgorithm.Process(bidiData); - var resolvedEmbeddingLevel = biDi.ResolveEmbeddingLevel(biDiData.Classes); + var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes); resolvedFlowDirection = (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; var processedRuns = new List(textRuns.Count); - CoalesceLevels(textRuns, biDi.ResolvedLevels, processedRuns); + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels, processedRuns); for (var index = 0; index < processedRuns.Count; index++) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index ae6df3a232..7fa9155b02 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using System.Threading; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { internal sealed class TextLineImpl : TextLine { - private TextRun[] _textRuns; + private static readonly ThreadLocal s_bidiReorderer = new(() => new BidiReorderer()); + + private readonly TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; private TextLineMetrics _textLineMetrics; @@ -993,185 +996,12 @@ namespace Avalonia.Media.TextFormatting { _textLineMetrics = CreateLineMetrics(); - BidiReorder(); + var bidiReorderer = s_bidiReorderer.Value!; + bidiReorderer.BidiReorder(_textRuns, _resolvedFlowDirection); return this; } - private static sbyte GetRunBidiLevel(TextRun run, FlowDirection flowDirection) - { - if (run is ShapedTextRun shapedTextCharacters) - { - return shapedTextCharacters.BidiLevel; - } - - var defaultLevel = flowDirection == FlowDirection.LeftToRight ? 0 : 1; - - return (sbyte)defaultLevel; - } - - private void BidiReorder() - { - if (_textRuns.Length == 0) - { - return; - } - - // Build up the collection of ordered runs. - var run = _textRuns[0]; - - OrderedBidiRun orderedRun = new(run, GetRunBidiLevel(run, _resolvedFlowDirection)); - - var current = orderedRun; - - for (var i = 1; i < _textRuns.Length; i++) - { - run = _textRuns[i]; - - current.Next = new OrderedBidiRun(run, GetRunBidiLevel(run, _resolvedFlowDirection)); - - current = current.Next; - } - - // Reorder them into visual order. - orderedRun = LinearReOrder(orderedRun); - - // Now perform a recursive reversal of each run. - // From the highest level found in the text to the lowest odd level on each line, including intermediate levels - // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. - // https://unicode.org/reports/tr9/#L2 - sbyte max = 0; - var min = sbyte.MaxValue; - - for (var i = 0; i < _textRuns.Length; i++) - { - var currentRun = _textRuns[i]; - - var level = GetRunBidiLevel(currentRun, _resolvedFlowDirection); - - if (level > max) - { - max = level; - } - - if ((level & 1) != 0 && level < min) - { - min = level; - } - } - - if (min > max) - { - min = max; - } - - if (max == 0 || (min == max && (max & 1) == 0)) - { - // Nothing to reverse. - return; - } - - // Now apply the reversal and replace the original contents. - var minLevelToReverse = max; - - while (minLevelToReverse >= min) - { - current = orderedRun; - - while (current != null) - { - if (current.Level >= minLevelToReverse && current.Level % 2 != 0) - { - if (current.Run is ShapedTextRun { IsReversed: false } shapedTextCharacters) - { - shapedTextCharacters.Reverse(); - } - } - - current = current.Next; - } - - minLevelToReverse--; - } - - var textRuns = new TextRun[_textRuns.Length]; - var index = 0; - - current = orderedRun; - - while (current != null) - { - textRuns[index++] = current.Run; - - current = current.Next; - } - - _textRuns = textRuns; - } - - /// - /// Reorders a series of runs from logical to visual order, returning the left most run. - /// - /// - /// The ordered bidi run. - /// The . - private static OrderedBidiRun LinearReOrder(OrderedBidiRun? run) - { - BidiRange? range = null; - - while (run != null) - { - var next = run.Next; - - while (range != null && range.Level > run.Level - && range.Previous != null && range.Previous.Level >= run.Level) - { - range = BidiRange.MergeWithPrevious(range); - } - - if (range != null && range.Level >= run.Level) - { - // Attach run to the range. - if ((run.Level & 1) != 0) - { - // Odd, range goes to the right of run. - run.Next = range.Left; - range.Left = run; - } - else - { - // Even, range goes to the left of run. - range.Right!.Next = run; - range.Right = run; - } - - range.Level = run.Level; - } - else - { - var r = new BidiRange(); - - r.Left = r.Right = run; - r.Level = run.Level; - r.Previous = range; - - range = r; - } - - run = next; - } - - while (range?.Previous != null) - { - range = BidiRange.MergeWithPrevious(range); - } - - // Terminate. - range!.Right!.Next = null; - - return range.Left!; - } - /// /// Tries to find the next character hit. /// @@ -1620,59 +1450,5 @@ namespace Avalonia.Media.TextFormatting return 0; } } - - private sealed class OrderedBidiRun - { - public OrderedBidiRun(TextRun run, sbyte level) - { - Run = run; - Level = level; - } - - public sbyte Level { get; } - - public TextRun Run { get; } - - public OrderedBidiRun? Next { get; set; } - } - - private sealed class BidiRange - { - public int Level { get; set; } - - public OrderedBidiRun? Left { get; set; } - - public OrderedBidiRun? Right { get; set; } - - public BidiRange? Previous { get; set; } - - public static BidiRange MergeWithPrevious(BidiRange range) - { - var previous = range.Previous; - - BidiRange left; - BidiRange right; - - if ((previous!.Level & 1) != 0) - { - // Odd, previous goes to the right of range. - left = range; - right = previous; - } - else - { - // Even, previous goes to the left of range. - left = previous; - right = range; - } - - // Stitch them - left.Right!.Next = right.Left; - previous.Left = left.Left; - previous.Right = right.Right; - - return previous; - } - } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index e770ba9e91..e960a510a9 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Runtime.CompilerServices; -using System.Threading; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 106079de8e..226e5ad6bd 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -11,6 +11,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// Represents a unicode string and all associated attributes /// for each character required for the bidirectional Unicode algorithm /// + /// To avoid allocations, this class is designed to be reused. internal sealed class BidiData { private ArrayBuilder _classes; diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index 1c11966a7d..3a22fc7b9c 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -18,7 +18,7 @@ namespace Avalonia.Utilities private const int MaxCoreClrArrayLength = 0x7FeFFFFF; // Starts out null, initialized on first Add. - private T[] _data; + private T[]? _data; private int _size; /// @@ -115,6 +115,16 @@ namespace Avalonia.Utilities return slice; } + /// + /// Appends an item. + /// + /// The item to append. + public void AddItem(T value) + { + var index = Length++; + _data![index] = value; + } + /// /// Clears the array. /// Allocated memory is left intact for future usage. From 4144be11fefdfbe632c49752dd5276395b5a41ae Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Tue, 17 Jan 2023 17:42:54 +0100 Subject: [PATCH 10/23] Pass GlyphInfo directly to GlyphRun --- src/Avalonia.Base/Media/GlyphRun.cs | 518 +++++++----------- .../Media/TextFormatting/GlyphInfo.cs | 36 ++ .../TextFormatting/InterWordJustification.cs | 2 +- .../Media/TextFormatting/ShapedBuffer.cs | 189 +------ .../Media/TextFormatting/ShapedTextRun.cs | 24 +- .../Media/TextFormatting/TextFormatterImpl.cs | 2 +- .../Platform/IPlatformRenderInterface.cs | 9 +- .../Composition/Server/FpsCounter.cs | 2 +- .../Utilities/BinarySearchExtension.cs | 3 +- .../HeadlessPlatformRenderInterface.cs | 3 +- .../Avalonia.Skia/PlatformRenderInterface.cs | 83 +-- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 42 +- .../Media/GlyphRunTests.cs | 14 +- .../VisualTree/MockRenderInterface.cs | 3 +- .../NullRenderingPlatform.cs | 3 +- .../Media/GlyphRunTests.cs | 10 +- .../Media/GlyphRunTests.cs | 21 +- .../TextFormatting/TextFormatterTests.cs | 10 +- .../Media/TextFormatting/TextLayoutTests.cs | 55 +- .../Media/TextFormatting/TextLineTests.cs | 30 +- .../Media/TextFormatting/TextShaperTests.cs | 10 +- .../MockPlatformRenderInterface.cs | 3 +- ...ould_Render_GlyphRun_Geometry.expected.png | Bin 4149 -> 4222 bytes 23 files changed, 390 insertions(+), 682 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index fc4bc6aa1c..b637c94d88 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using System.Runtime.InteropServices; +using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; using Avalonia.Utilities; @@ -11,64 +13,112 @@ namespace Avalonia.Media /// public sealed class GlyphRun : IDisposable { - private static readonly IComparer s_ascendingComparer = Comparer.Default; - private static readonly IComparer s_descendingComparer = new ReverseComparer(); - private IGlyphRunImpl? _glyphRunImpl; - private IGlyphTypeface _glyphTypeface; private double _fontRenderingEmSize; private int _biDiLevel; private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; - private ReadOnlyMemory _characters; - private IReadOnlyList _glyphIndices; - private IReadOnlyList? _glyphAdvances; - private IReadOnlyList? _glyphOffsets; - private IReadOnlyList? _glyphClusters; + private IReadOnlyList _glyphInfos; + private bool _hasOneCharPerCluster; // if true, character index and cluster are similar /// - /// Initializes a new instance of the class by specifying properties of the class. + /// Initializes a new instance of the class by specifying properties of the class. /// /// The glyph typeface. /// The rendering em size. - /// The glyph indices. - /// The glyph advances. - /// The glyph offsets. /// The characters. - /// The glyph clusters. + /// The glyph indices. /// The bidi level. public GlyphRun( IGlyphTypeface glyphTypeface, double fontRenderingEmSize, ReadOnlyMemory characters, IReadOnlyList glyphIndices, - IReadOnlyList? glyphAdvances = null, - IReadOnlyList? glyphOffsets = null, - IReadOnlyList? glyphClusters = null, + int biDiLevel = 0) + : this(glyphTypeface, fontRenderingEmSize, characters, + CreateGlyphInfos(glyphIndices, fontRenderingEmSize, glyphTypeface), biDiLevel) + { + _hasOneCharPerCluster = true; + } + + /// + /// Initializes a new instance of the class by specifying properties of the class. + /// + /// The glyph typeface. + /// The rendering em size. + /// The characters. + /// The list of glyphs used. + /// The bidi level. + public GlyphRun( + IGlyphTypeface glyphTypeface, + double fontRenderingEmSize, + ReadOnlyMemory characters, + IReadOnlyList glyphInfos, int biDiLevel = 0) { - _glyphTypeface = glyphTypeface; + GlyphTypeface = glyphTypeface; _fontRenderingEmSize = fontRenderingEmSize; _characters = characters; - _glyphIndices = glyphIndices; + _glyphInfos = glyphInfos; - _glyphAdvances = glyphAdvances; + _biDiLevel = biDiLevel; + } - _glyphOffsets = glyphOffsets; + private static IReadOnlyList CreateGlyphInfos(IReadOnlyList glyphIndices, + double fontRenderingEmSize, IGlyphTypeface glyphTypeface) + { + var glyphIndexSpan = ListToSpan(glyphIndices); + var glyphAdvances = glyphTypeface.GetGlyphAdvances(glyphIndexSpan); - _glyphClusters = glyphClusters; + var glyphInfos = new GlyphInfo[glyphIndexSpan.Length]; + var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; - _biDiLevel = biDiLevel; + for (var i = 0; i < glyphIndexSpan.Length; ++i) + { + glyphInfos[i] = new GlyphInfo(glyphIndexSpan[i], i, glyphAdvances[i] * scale); + } + + return glyphInfos; + } + + private static ReadOnlySpan ListToSpan(IReadOnlyList list) + { + var count = list.Count; + + if (count == 0) + { + return default; + } + + if (list is ushort[] array) + { + return array.AsSpan(); + } + +#if NET6_0_OR_GREATER + if (list is List concreteList) + { + return CollectionsMarshal.AsSpan(concreteList); + } +#endif + + array = new ushort[count]; + for (var i = 0; i < count; ++i) + { + array[i] = list[i]; + } + + return array.AsSpan(); } /// /// Gets the for the . /// - public IGlyphTypeface GlyphTypeface => _glyphTypeface; + public IGlyphTypeface GlyphTypeface { get; } /// /// Gets or sets the em size used for rendering the . @@ -88,56 +138,17 @@ namespace Avalonia.Media /// /// public GlyphRunMetrics Metrics - { - get - { - _glyphRunMetrics ??= CreateGlyphRunMetrics(); - - return _glyphRunMetrics.Value; - } - } + => _glyphRunMetrics ??= CreateGlyphRunMetrics(); /// /// Gets or sets the baseline origin of the. /// public Point BaselineOrigin { - get - { - _baselineOrigin ??= CalculateBaselineOrigin(); - - return _baselineOrigin.Value; - } + get => _baselineOrigin ??= CalculateBaselineOrigin(); set => Set(ref _baselineOrigin, value); } - /// - /// Gets or sets an array of values that represent the glyph indices in the rendering physical font. - /// - public IReadOnlyList GlyphIndices - { - get => _glyphIndices; - set => Set(ref _glyphIndices, value); - } - - /// - /// Gets or sets an array of values that represent the advances corresponding to the glyph indices. - /// - public IReadOnlyList? GlyphAdvances - { - get => _glyphAdvances; - set => Set(ref _glyphAdvances, value); - } - - /// - /// Gets or sets an array of values representing the offsets of the glyphs in the . - /// - public IReadOnlyList? GlyphOffsets - { - get => _glyphOffsets; - set => Set(ref _glyphOffsets, value); - } - /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . /// @@ -148,12 +159,16 @@ namespace Avalonia.Media } /// - /// Gets or sets a list of values representing a mapping from character index to glyph index. + /// Gets or sets the list of glyphs to use to render this run. /// - public IReadOnlyList? GlyphClusters + public IReadOnlyList GlyphInfos { - get => _glyphClusters; - set => Set(ref _glyphClusters, value); + get => _glyphInfos; + set + { + Set(ref _glyphInfos, value); + _hasOneCharPerCluster = false; + } } /// @@ -179,17 +194,7 @@ namespace Avalonia.Media /// The platform implementation of the . /// public IGlyphRunImpl GlyphRunImpl - { - get - { - if (_glyphRunImpl == null) - { - Initialize(); - } - - return _glyphRunImpl!; - } - } + => _glyphRunImpl ??= CreateGlyphRunImpl(); /// /// Obtains geometry for the glyph run. @@ -221,38 +226,32 @@ namespace Avalonia.Media if (IsLeftToRight) { - if (GlyphClusters != null) + if (characterIndex < Metrics.FirstCluster) { - if (characterIndex < Metrics.FirstCluster) - { - return 0; - } + return 0; + } - if (characterIndex > Metrics.LastCluster) - { - return Metrics.WidthIncludingTrailingWhitespace; - } + if (characterIndex > Metrics.LastCluster) + { + return Metrics.WidthIncludingTrailingWhitespace; } var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null) - { - var currentCluster = GlyphClusters[glyphIndex]; + var currentCluster = _glyphInfos[glyphIndex].GlyphCluster; - //Move to the end of the glyph cluster - if (characterHit.TrailingLength > 0) + //Move to the end of the glyph cluster + if (characterHit.TrailingLength > 0) + { + while (glyphIndex + 1 < _glyphInfos.Count && _glyphInfos[glyphIndex + 1].GlyphCluster == currentCluster) { - while (glyphIndex + 1 < GlyphClusters.Count && GlyphClusters[glyphIndex + 1] == currentCluster) - { - glyphIndex++; - } + glyphIndex++; } } for (var i = 0; i < glyphIndex; i++) { - distance += GetGlyphAdvance(i, out _); + distance += _glyphInfos[i].GlyphAdvance; } return distance; @@ -262,22 +261,19 @@ namespace Avalonia.Media //RightToLeft var glyphIndex = FindGlyphIndex(characterIndex); - if (GlyphClusters != null && GlyphClusters.Count > 0) + if (characterIndex > Metrics.LastCluster) { - if (characterIndex > Metrics.LastCluster) - { - return 0; - } + return 0; + } - if (characterIndex <= Metrics.FirstCluster) - { - return Size.Width; - } + if (characterIndex <= Metrics.FirstCluster) + { + return Size.Width; } - for (var i = glyphIndex + 1; i < GlyphIndices.Count; i++) + for (var i = glyphIndex + 1; i < _glyphInfos.Count; i++) { - distance += GetGlyphAdvance(i, out _); + distance += _glyphInfos[i].GlyphAdvance; } return Size.Width - distance; @@ -322,11 +318,12 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (var index = 0; index < GlyphIndices.Count; index++) + for (var index = 0; index < _glyphInfos.Count; index++) { - var advance = GetGlyphAdvance(index, out var cluster); + var glyphInfo = _glyphInfos[index]; + var advance = glyphInfo.GlyphAdvance; - characterIndex = cluster; + characterIndex = glyphInfo.GlyphCluster; if (distance > currentX && distance <= currentX + advance) { @@ -340,11 +337,12 @@ namespace Avalonia.Media { currentX = Size.Width; - for (var index = GlyphIndices.Count - 1; index >= 0; index--) + for (var index = _glyphInfos.Count - 1; index >= 0; index--) { - var advance = GetGlyphAdvance(index, out var cluster); + var glyphInfo = _glyphInfos[index]; + var advance = glyphInfo.GlyphAdvance; - characterIndex = cluster; + characterIndex = glyphInfo.GlyphCluster; var offsetX = currentX - advance; @@ -424,7 +422,7 @@ namespace Avalonia.Media /// public int FindGlyphIndex(int characterIndex) { - if (GlyphClusters == null || GlyphClusters.Count == 0) + if (_hasOneCharPerCluster) { return characterIndex; } @@ -433,7 +431,7 @@ namespace Avalonia.Media { if (IsLeftToRight) { - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } return 0; @@ -446,15 +444,13 @@ namespace Avalonia.Media return 0; } - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } - var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; - - var clusters = GlyphClusters; + var comparer = IsLeftToRight ? GlyphInfo.ClusterAscendingComparer : GlyphInfo.ClusterDescendingComparer; // Find the start of the cluster at the character index. - var start = clusters.BinarySearch(characterIndex, comparer); + var start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer); // No cluster found. if (start < 0) @@ -463,40 +459,38 @@ namespace Avalonia.Media { characterIndex--; - start = clusters.BinarySearch(characterIndex, comparer); + start = _glyphInfos.BinarySearch(new GlyphInfo(default, characterIndex, default), comparer); } if (start < 0) { - goto result; + return 0; } } if (IsLeftToRight) { - while (start > 0 && clusters[start - 1] == clusters[start]) + while (start > 0 && _glyphInfos[start - 1].GlyphCluster == _glyphInfos[start].GlyphCluster) { start--; } } else { - while (start + 1 < clusters.Count && clusters[start + 1] == clusters[start]) + while (start + 1 < _glyphInfos.Count && _glyphInfos[start + 1].GlyphCluster == _glyphInfos[start].GlyphCluster) { start++; } } - result: - if (start < 0) { return 0; } - if (start > GlyphIndices.Count - 1) + if (start > _glyphInfos.Count - 1) { - return GlyphIndices.Count - 1; + return _glyphInfos.Count - 1; } return start; @@ -516,14 +510,14 @@ namespace Avalonia.Media var glyphIndex = FindGlyphIndex(index); - if (GlyphClusters == null) + if (_hasOneCharPerCluster) { - width = GetGlyphAdvance(index, out _); + width = _glyphInfos[index].GlyphAdvance; return new CharacterHit(glyphIndex, 1); } - var cluster = GlyphClusters[glyphIndex]; + var cluster = _glyphInfos[glyphIndex].GlyphCluster; var nextCluster = cluster; @@ -531,13 +525,13 @@ namespace Avalonia.Media while (nextCluster == cluster) { - width += GetGlyphAdvance(currentIndex, out _); + width += _glyphInfos[currentIndex].GlyphAdvance; if (IsLeftToRight) { currentIndex++; - if (currentIndex == GlyphClusters.Count) + if (currentIndex == _glyphInfos.Count) { break; } @@ -552,7 +546,7 @@ namespace Avalonia.Media } } - nextCluster = GlyphClusters[currentIndex]; + nextCluster = _glyphInfos[currentIndex].GlyphCluster; } var clusterLength = Math.Max(0, nextCluster - cluster); @@ -565,9 +559,9 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (int i = 1; i < GlyphClusters.Count; i++) + for (int i = 1; i < _glyphInfos.Count; i++) { - nextCluster = GlyphClusters[i]; + nextCluster = _glyphInfos[i].GlyphCluster; if (currentCluster > cluster) { @@ -583,9 +577,9 @@ namespace Avalonia.Media } else { - for (int i = GlyphClusters.Count - 1; i >= 0; i--) + for (int i = _glyphInfos.Count - 1; i >= 0; i--) { - nextCluster = GlyphClusters[i]; + nextCluster = _glyphInfos[i].GlyphCluster; if (currentCluster > cluster) { @@ -613,26 +607,6 @@ namespace Avalonia.Media return new CharacterHit(cluster, clusterLength); } - /// - /// Gets a glyph's width. - /// - /// The glyph index. - /// The current cluster. - /// The glyph's width. - private double GetGlyphAdvance(int index, out int cluster) - { - cluster = GlyphClusters != null ? GlyphClusters[index] : index; - - if (GlyphAdvances != null) - { - return GlyphAdvances[index]; - } - - var glyph = GlyphIndices[index]; - - return GlyphTypeface.GetGlyphAdvance(glyph) * Scale; - } - /// /// Calculates the default baseline origin of the . /// @@ -644,20 +618,17 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { - int firstCluster = 0, lastCluster = 0; + int firstCluster, lastCluster; - if (_glyphClusters != null && _glyphClusters.Count > 0) + if (Characters.IsEmpty) { - firstCluster = _glyphClusters[0]; - lastCluster = _glyphClusters[_glyphClusters.Count - 1]; + firstCluster = 0; + lastCluster = 0; } else { - if (!Characters.IsEmpty) - { - firstCluster = 0; - lastCluster = Characters.Length - 1; - } + firstCluster = _glyphInfos[0].GlyphCluster; + lastCluster = _glyphInfos[_glyphInfos.Count - 1].GlyphCluster; } if (!IsLeftToRight) @@ -671,9 +642,9 @@ namespace Avalonia.Media var trailingWhitespaceLength = GetTrailingWhitespaceLength(isReversed, out var newLineLength, out var glyphCount); - for (var index = 0; index < GlyphIndices.Count; index++) + for (var index = 0; index < _glyphInfos.Count; index++) { - var advance = GetGlyphAdvance(index, out _); + var advance = _glyphInfos[index].GlyphAdvance; widthIncludingTrailingWhitespace += advance; } @@ -684,14 +655,14 @@ namespace Avalonia.Media { for (var index = 0; index < glyphCount; index++) { - width -= GetGlyphAdvance(index, out _); + width -= _glyphInfos[index].GlyphAdvance; } } else { - for (var index = GlyphIndices.Count - glyphCount; index < GlyphIndices.Count; index++) + for (var index = _glyphInfos.Count - glyphCount; index < _glyphInfos.Count; index++) { - width -= GetGlyphAdvance(index, out _); + width -= _glyphInfos[index].GlyphAdvance; } } @@ -710,7 +681,7 @@ namespace Avalonia.Media { if (isReversed) { - return GetTralingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); + return GetTrailingWhitespaceLengthRightToLeft(out newLineLength, out glyphCount); } glyphCount = 0; @@ -720,84 +691,59 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - if (GlyphClusters == null) - { - for (var i = charactersSpan.Length - 1; i >= 0;) - { - var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); - - if (!codepoint.IsWhiteSpace) - { - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength++; - } - - trailingWhitespaceLength++; + var characterIndex = charactersSpan.Length - 1; - i -= count; - glyphCount++; - } - } - else + for (var i = _glyphInfos.Count - 1; i >= 0; i--) { - var characterIndex = charactersSpan.Length - 1; + var currentCluster = _glyphInfos[i].GlyphCluster; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = GlyphClusters.Count - 1; i >= 0; i--) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); + characterIndex -= characterLength; - characterIndex -= characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + while (i - 1 >= 0) + { + var nextCluster = _glyphInfos[i - 1].GlyphCluster; - while (i - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[i - 1]; + clusterLength++; + i--; - if (currentCluster == nextCluster) + if(characterIndex >= 0) { - clusterLength++; - i--; - - if(characterIndex >= 0) - { - codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); + codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out characterLength); - characterIndex -= characterLength; - } - - continue; + characterIndex -= characterLength; } - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount++; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount++; } } return trailingWhitespaceLength; } - private int GetTralingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) + private int GetTrailingWhitespaceLengthRightToLeft(out int newLineLength, out int glyphCount) { glyphCount = 0; newLineLength = 0; @@ -806,71 +752,46 @@ namespace Avalonia.Media if (!charactersSpan.IsEmpty) { - if (GlyphClusters == null) - { - for (var i = 0; i < charactersSpan.Length;) - { - var codepoint = Codepoint.ReadAt(charactersSpan, i, out var count); + var characterIndex = 0; - if (!codepoint.IsWhiteSpace) - { - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength++; - } - - trailingWhitespaceLength++; - - i += count; - glyphCount++; - } - } - else + for (var i = 0; i < _glyphInfos.Count; i++) { - var characterIndex = 0; + var currentCluster = _glyphInfos[i].GlyphCluster; + var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); - for (var i = 0; i < GlyphClusters.Count; i++) - { - var currentCluster = GlyphClusters[i]; - var codepoint = Codepoint.ReadAt(charactersSpan, characterIndex, out var characterLength); + characterIndex += characterLength; - characterIndex += characterLength; + if (!codepoint.IsWhiteSpace) + { + break; + } - if (!codepoint.IsWhiteSpace) - { - break; - } + var clusterLength = 1; - var clusterLength = 1; + var j = i; - var j = i; + while (j - 1 >= 0) + { + var nextCluster = _glyphInfos[--j].GlyphCluster; - while (j - 1 >= 0) + if (currentCluster == nextCluster) { - var nextCluster = GlyphClusters[--j]; - - if (currentCluster == nextCluster) - { - clusterLength++; + clusterLength++; - continue; - } - - break; - } - - if (codepoint.IsBreakChar) - { - newLineLength += clusterLength; + continue; } - trailingWhitespaceLength += clusterLength; + break; + } - glyphCount += clusterLength; + if (codepoint.IsBreakChar) + { + newLineLength += clusterLength; } + + trailingWhitespaceLength += clusterLength; + + glyphCount += clusterLength; } } @@ -890,44 +811,17 @@ namespace Avalonia.Media field = value; } - /// - /// Initializes the . - /// - private void Initialize() + private IGlyphRunImpl CreateGlyphRunImpl() { - if (GlyphIndices == null) - { - throw new InvalidOperationException(); - } - - var glyphCount = GlyphIndices.Count; - - if (GlyphAdvances != null && GlyphAdvances.Count > 0 && GlyphAdvances.Count != glyphCount) - { - throw new InvalidOperationException(); - } - - if (GlyphOffsets != null && GlyphOffsets.Count > 0 && GlyphOffsets.Count != glyphCount) - { - throw new InvalidOperationException(); - } - var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - _glyphRunImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphIndices, GlyphAdvances, GlyphOffsets); + return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos); } public void Dispose() { _glyphRunImpl?.Dispose(); - } - - private class ReverseComparer : IComparer - { - public int Compare(T? x, T? y) - { - return Comparer.Default.Compare(y, x); - } + _glyphRunImpl = null; } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs b/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs new file mode 100644 index 0000000000..36a07721a6 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/GlyphInfo.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Represents a single glyph. + /// + public readonly record struct GlyphInfo(ushort GlyphIndex, int GlyphCluster, double GlyphAdvance, Vector GlyphOffset = default) + { + internal static Comparer ClusterAscendingComparer { get; } = + Comparer.Create((x, y) => x.GlyphCluster.CompareTo(y.GlyphCluster)); + + internal static Comparer ClusterDescendingComparer { get; } = + Comparer.Create((x, y) => y.GlyphCluster.CompareTo(x.GlyphCluster)); + + /// + /// Get the glyph index. + /// + public ushort GlyphIndex { get; } = GlyphIndex; + + /// + /// Get the glyph cluster. + /// + public int GlyphCluster { get; } = GlyphCluster; + + /// + /// Get the glyph advance. + /// + public double GlyphAdvance { get; } = GlyphAdvance; + + /// + /// Get the glyph offset. + /// + public Vector GlyphOffset { get; } = GlyphOffset; + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index b518d47a6d..6bfcfc06f8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -111,7 +111,7 @@ namespace Avalonia.Media.TextFormatting shapedBuffer.GlyphInfos[glyphIndex] = new GlyphInfo(glyphInfo.GlyphIndex, glyphInfo.GlyphCluster, glyphInfo.GlyphAdvance + spacing); } - glyphRun.GlyphAdvances = shapedBuffer.GlyphAdvances; + glyphRun.GlyphInfos = shapedBuffer.GlyphInfos; } currentPosition += textRun.Length; diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs index 41bba2cd09..f29bdd4459 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedBuffer.cs @@ -1,14 +1,13 @@ using System; using System.Buffers; +using System.Collections; using System.Collections.Generic; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { - public sealed class ShapedBuffer : IList, IDisposable + public sealed class ShapedBuffer : IReadOnlyList, IDisposable { - private static readonly IComparer s_clusterComparer = new CompareClusters(); - private GlyphInfo[]? _rentedBuffer; public ShapedBuffer(ReadOnlyMemory text, int bufferLength, IGlyphTypeface glyphTypeface, double fontRenderingEmSize, sbyte bidiLevel) @@ -42,14 +41,6 @@ namespace Avalonia.Media.TextFormatting public sbyte BidiLevel { get; } public bool IsLeftToRight => (BidiLevel & 1) == 0; - - public IReadOnlyList GlyphIndices => new GlyphIndexList(GlyphInfos); - - public IReadOnlyList GlyphClusters => new GlyphClusterList(GlyphInfos); - - public IReadOnlyList GlyphAdvances => new GlyphAdvanceList(GlyphInfos); - - public IReadOnlyList GlyphOffsets => new GlyphOffsetList(GlyphInfos); public ReadOnlyMemory Text { get; } @@ -73,13 +64,13 @@ namespace Avalonia.Media.TextFormatting } - var comparer = s_clusterComparer; + var comparer = GlyphInfo.ClusterAscendingComparer; - var clusters = GlyphInfos.Span; + var glyphInfos = GlyphInfos.Span; - var searchValue = new GlyphInfo(0, characterIndex); + var searchValue = new GlyphInfo(default, characterIndex, default); - var start = clusters.BinarySearch(searchValue, comparer); + var start = glyphInfos.BinarySearch(searchValue, comparer); if (start < 0) { @@ -87,9 +78,9 @@ namespace Avalonia.Media.TextFormatting { characterIndex--; - searchValue = new GlyphInfo(0, characterIndex); + searchValue = new GlyphInfo(default, characterIndex, default); - start = clusters.BinarySearch(searchValue, comparer); + start = glyphInfos.BinarySearch(searchValue, comparer); } if (start < 0) @@ -98,7 +89,7 @@ namespace Avalonia.Media.TextFormatting } } - while (start > 0 && clusters[start - 1].GlyphCluster == clusters[start].GlyphCluster) + while (start > 0 && glyphInfos[start - 1].GlyphCluster == glyphInfos[start].GlyphCluster) { start--; } @@ -118,8 +109,8 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(this, null); } - var firstCluster = GlyphClusters[0]; - var lastCluster = GlyphClusters[GlyphClusters.Count - 1]; + var firstCluster = GlyphInfos[0].GlyphCluster; + var lastCluster = GlyphInfos[GlyphInfos.Length - 1].GlyphCluster; var start = firstCluster < lastCluster ? firstCluster : lastCluster; @@ -134,9 +125,7 @@ namespace Avalonia.Media.TextFormatting return new SplitResult(first, second); } - int ICollection.Count => throw new NotImplementedException(); - - bool ICollection.IsReadOnly => true; + int IReadOnlyCollection.Count => GlyphInfos.Length; public GlyphInfo this[int index] { @@ -144,130 +133,9 @@ namespace Avalonia.Media.TextFormatting set => GlyphInfos[index] = value; } - int IList.IndexOf(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void IList.Insert(int index, GlyphInfo item) - { - throw new NotImplementedException(); - } - - void IList.RemoveAt(int index) - { - throw new NotImplementedException(); - } - - void ICollection.Add(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void ICollection.Clear() - { - throw new NotImplementedException(); - } - - bool ICollection.Contains(GlyphInfo item) - { - throw new NotImplementedException(); - } - - void ICollection.CopyTo(GlyphInfo[] array, int arrayIndex) - { - throw new NotImplementedException(); - } - - bool ICollection.Remove(GlyphInfo item) - { - throw new NotImplementedException(); - } public IEnumerator GetEnumerator() => GlyphInfos.GetEnumerator(); - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - - private class CompareClusters : IComparer - { - private static readonly Comparer s_intClusterComparer = Comparer.Default; - - public int Compare(GlyphInfo x, GlyphInfo y) - { - return s_intClusterComparer.Compare(x.GlyphCluster, y.GlyphCluster); - } - } - - private readonly struct GlyphAdvanceList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphAdvanceList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public double this[int index] => _glyphInfos[index].GlyphAdvance; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphIndexList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphIndexList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public ushort this[int index] => _glyphInfos[index].GlyphIndex; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphClusterList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphClusterList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public int this[int index] => _glyphInfos[index].GlyphCluster; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } - - private readonly struct GlyphOffsetList : IReadOnlyList - { - private readonly ArraySlice _glyphInfos; - - public GlyphOffsetList(ArraySlice glyphInfos) - { - _glyphInfos = glyphInfos; - } - - public Vector this[int index] => _glyphInfos[index].GlyphOffset; - - public int Count => _glyphInfos.Length; - - public IEnumerator GetEnumerator() => new ImmutableReadOnlyListStructEnumerator(this); - - System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); public void Dispose() { @@ -279,35 +147,4 @@ namespace Avalonia.Media.TextFormatting } } } - - public readonly record struct GlyphInfo - { - public GlyphInfo(ushort glyphIndex, int glyphCluster, double glyphAdvance = 0, Vector glyphOffset = default) - { - GlyphIndex = glyphIndex; - GlyphAdvance = glyphAdvance; - GlyphCluster = glyphCluster; - GlyphOffset = glyphOffset; - } - - /// - /// Get the glyph index. - /// - public ushort GlyphIndex { get; } - - /// - /// Get the glyph cluster. - /// - public int GlyphCluster { get; } - - /// - /// Get the glyph advance. - /// - public double GlyphAdvance { get; } - - /// - /// Get the glyph offset. - /// - public Vector GlyphOffset { get; } - } } diff --git a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs index 583f2e49f1..d444a58297 100644 --- a/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs +++ b/src/Avalonia.Base/Media/TextFormatting/ShapedTextRun.cs @@ -40,25 +40,14 @@ namespace Avalonia.Media.TextFormatting public override Size Size => GlyphRun.Size; - public GlyphRun GlyphRun - { - get - { - if(_glyphRun is null) - { - _glyphRun = CreateGlyphRun(); - } - - return _glyphRun; - } - } + public GlyphRun GlyphRun => _glyphRun ??= CreateGlyphRun(); /// public override void Draw(DrawingContext drawingContext, Point origin) { using (drawingContext.PushPreTransform(Matrix.CreateTranslation(origin))) { - if (GlyphRun.GlyphIndices.Count == 0) + if (GlyphRun.GlyphInfos.Count == 0) { return; } @@ -117,7 +106,7 @@ namespace Avalonia.Media.TextFormatting for (var i = 0; i < ShapedBuffer.Length; i++) { - var advance = ShapedBuffer.GlyphAdvances[i]; + var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; if (currentWidth + advance > availableWidth) { @@ -141,7 +130,7 @@ namespace Avalonia.Media.TextFormatting for (var i = ShapedBuffer.Length - 1; i >= 0; i--) { - var advance = ShapedBuffer.GlyphAdvances[i]; + var advance = ShapedBuffer.GlyphInfos[i].GlyphAdvance; if (width + advance > availableWidth) { @@ -195,10 +184,7 @@ namespace Avalonia.Media.TextFormatting ShapedBuffer.GlyphTypeface, ShapedBuffer.FontRenderingEmSize, Text, - ShapedBuffer.GlyphIndices, - ShapedBuffer.GlyphAdvances, - ShapedBuffer.GlyphOffsets, - ShapedBuffer.GlyphClusters, + ShapedBuffer, BidiLevel); } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index f3cc0a714e..8ffe3e5da2 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -611,7 +611,7 @@ namespace Avalonia.Media.TextFormatting var properties = paragraphProperties.DefaultTextRunProperties; var glyphTypeface = properties.Typeface.GlyphTypeface; var glyph = glyphTypeface.GetGlyph(s_empty[0]); - var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; + var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex, 0.0) }; var shapedBuffer = new ShapedBuffer(s_empty.AsMemory(), glyphInfos, glyphTypeface, properties.FontRenderingEmSize, (sbyte)flowDirection); diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 1828f24aff..9f4e96da25 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Metadata; namespace Avalonia.Platform @@ -166,11 +167,9 @@ namespace Avalonia.Platform /// /// The glyph typeface. /// The font rendering em size. - /// The glyph indices. - /// The glyph advances. - /// The glyph offsets. - /// - IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList? glyphAdvances, IReadOnlyList? glyphOffsets); + /// The list of glyphs. + /// An . + IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos); /// /// Creates a backend-specific object using a low-level API graphics context diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 18cb7a6308..32923a5257 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -32,7 +32,7 @@ internal class FpsCounter { var s = new string((char)c, 1); var glyph = typeface.GetGlyph((uint)(s[0])); - _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.ToArray(), new ushort[] { glyph }); + _runs[c - FirstChar] = new GlyphRun(typeface, 18, s.AsMemory(), new ushort[] { glyph }); } } diff --git a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs index b7060d2e21..defc9b1639 100644 --- a/src/Avalonia.Base/Utilities/BinarySearchExtension.cs +++ b/src/Avalonia.Base/Utilities/BinarySearchExtension.cs @@ -14,7 +14,6 @@ // under the License. // Copied from: https://github.com/toptensoftware/RichTextKit -using System; using System.Collections.Generic; namespace Avalonia.Utilities @@ -39,7 +38,7 @@ namespace Avalonia.Utilities /// The value to search for /// The comparer /// The index of the found item; otherwise the bitwise complement of the index of the next larger item - public static int BinarySearch(this IReadOnlyList list, T value, IComparer comparer) where T : IComparable + public static int BinarySearch(this IReadOnlyList list, T value, IComparer comparer) { return list.BinarySearch(0, list.Count, value, comparer); } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index cb79cc85db..e368ddc373 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -9,6 +9,7 @@ using Avalonia.Rendering; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; namespace Avalonia.Headless { @@ -118,7 +119,7 @@ namespace Avalonia.Headless return new HeadlessGeometryStub(new Rect(glyphRun.Size)); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return new HeadlessGlyphRunStub(); } diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index b202b60cdf..3fb7491898 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -1,16 +1,11 @@ using System; -using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading; - -using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.Platform; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using SkiaSharp; namespace Avalonia.Skia @@ -88,9 +83,9 @@ namespace Avalonia.Skia var (currentX, currentY) = glyphRun.BaselineOrigin; - for (var i = 0; i < glyphRun.GlyphIndices.Count; i++) + for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { - var glyph = glyphRun.GlyphIndices[i]; + var glyph = glyphRun.GlyphInfos[i].GlyphIndex; var glyphPath = skFont.GetGlyphPath(glyph); if (!glyphPath.IsEmpty) @@ -98,14 +93,7 @@ namespace Avalonia.Skia path.AddPath(glyphPath, (float)currentX, (float)currentY); } - if (glyphRun.GlyphAdvances != null) - { - currentX += glyphRun.GlyphAdvances[i]; - } - else - { - currentX += glyphPath.Bounds.Right; - } + currentX += glyphRun.GlyphInfos[i].GlyphAdvance; } return new StreamGeometryImpl(path); @@ -213,17 +201,16 @@ namespace Avalonia.Skia return new WriteableBitmapImpl(size, dpi, format, alphaFormat); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, - IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { if (glyphTypeface == null) { throw new ArgumentNullException(nameof(glyphTypeface)); } - if (glyphIndices == null) + if (glyphInfos == null) { - throw new ArgumentNullException(nameof(glyphIndices)); + throw new ArgumentNullException(nameof(glyphInfos)); } var glyphTypefaceImpl = glyphTypeface as GlyphTypefaceImpl; @@ -242,59 +229,25 @@ namespace Avalonia.Skia var builder = new SKTextBlobBuilder(); - var count = glyphIndices.Count; + var count = glyphInfos.Count; - if (glyphOffsets != null && glyphAdvances != null) - { - var runBuffer = builder.AllocatePositionedRun(font, count); + var runBuffer = builder.AllocatePositionedRun(font, count); - var glyphSpan = runBuffer.GetGlyphSpan(); - var positionSpan = runBuffer.GetPositionSpan(); + var glyphSpan = runBuffer.GetGlyphSpan(); + var positionSpan = runBuffer.GetPositionSpan(); - var currentX = 0.0; + var currentX = 0.0; - for (int i = 0; i < glyphOffsets.Count; i++) - { - var offset = glyphOffsets[i]; - - glyphSpan[i] = glyphIndices[i]; - - positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); - - currentX += glyphAdvances[i]; - } - } - else + for (int i = 0; i < count; i++) { - if (glyphAdvances != null) - { - var runBuffer = builder.AllocateHorizontalRun(font, count, 0); - - var glyphSpan = runBuffer.GetGlyphSpan(); - var positionSpan = runBuffer.GetPositionSpan(); - - var currentX = 0.0; + var glyphInfo = glyphInfos[i]; + var offset = glyphInfo.GlyphOffset; - for (int i = 0; i < glyphAdvances.Count; i++) - { - glyphSpan[i] = glyphIndices[i]; + glyphSpan[i] = glyphInfo.GlyphIndex; - positionSpan[i] = (float)currentX; + positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); - currentX += glyphAdvances[i]; - } - } - else - { - var runBuffer = builder.AllocateRun(font, count, 0, 0); - - var glyphSpan = runBuffer.GetGlyphSpan(); - - for (int i = 0; i < glyphIndices.Count; i++) - { - glyphSpan[i] = glyphIndices[i]; - } - } + currentX += glyphInfo.GlyphAdvance; } return new GlyphRunImpl(builder.Build()); diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 5887ba2172..d9cd0590fc 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -7,6 +7,7 @@ using Avalonia.Direct2D1.Media; using Avalonia.Direct2D1.Media.Imaging; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; using SharpDX.DirectWrite; using GlyphRun = Avalonia.Media.GlyphRun; @@ -157,12 +158,11 @@ namespace Avalonia.Direct2D1 public IGeometryImpl CreateGeometryGroup(FillRule fillRule, IReadOnlyList children) => new GeometryGroupImpl(fillRule, children); public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => new CombinedGeometryImpl(combineMode, g1, g2); - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, - IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { var glyphTypefaceImpl = (GlyphTypefaceImpl)glyphTypeface; - var glyphCount = glyphIndices.Count; + var glyphCount = glyphInfos.Count; var run = new SharpDX.DirectWrite.GlyphRun { @@ -174,44 +174,23 @@ namespace Avalonia.Direct2D1 for (var i = 0; i < glyphCount; i++) { - indices[i] = (short)glyphIndices[i]; + indices[i] = (short)glyphInfos[i].GlyphIndex; } run.Indices = indices; run.Advances = new float[glyphCount]; - var scale = (float)(fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight); - - if (glyphAdvances == null) - { - for (var i = 0; i < glyphCount; i++) - { - var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[i]) * scale; - - run.Advances[i] = advance; - } - } - else - { - for (var i = 0; i < glyphCount; i++) - { - var advance = (float)glyphAdvances[i]; - - run.Advances[i] = advance; - } - } - - if (glyphOffsets == null) + for (var i = 0; i < glyphCount; i++) { - return new GlyphRunImpl(run); + run.Advances[i] = (float)glyphInfos[i].GlyphAdvance; } run.Offsets = new GlyphOffset[glyphCount]; for (var i = 0; i < glyphCount; i++) { - var (x, y) = glyphOffsets[i]; + var (x, y) = glyphInfos[i].GlyphOffset; run.Offsets[i] = new GlyphOffset { @@ -254,11 +233,12 @@ namespace Avalonia.Direct2D1 using (var sink = pathGeometry.Open()) { - var glyphs = new short[glyphRun.GlyphIndices.Count]; + var glyphInfos = glyphRun.GlyphInfos; + var glyphs = new short[glyphInfos.Count]; - for (int i = 0; i < glyphRun.GlyphIndices.Count; i++) + for (int i = 0; i < glyphInfos.Count; i++) { - glyphs[i] = (short)glyphRun.GlyphIndices[i]; + glyphs[i] = (short)glyphInfos[i].GlyphIndex; } glyphTypeface.FontFace.GetGlyphRunOutline((float)glyphRun.FontRenderingEmSize, glyphs, null, null, false, !glyphRun.IsLeftToRight, sink); diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 363fb3f5b3..3573ba6b07 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -1,5 +1,7 @@ -using System.Linq; +using System; +using System.Linq; using Avalonia.Media; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Utilities; @@ -179,12 +181,14 @@ namespace Avalonia.Base.UnitTests.Media private static GlyphRun CreateGlyphRun(double[] glyphAdvances, int[] glyphClusters, int bidiLevel = 0) { var count = glyphAdvances.Length; - var glyphIndices = new ushort[count]; - var characters = Enumerable.Repeat('a', count).ToArray(); + var glyphInfos = new GlyphInfo[count]; + for (var i = 0; i < count; ++i) + { + glyphInfos[i] = new GlyphInfo(0, glyphClusters[i], glyphAdvances[i]); + } - return new GlyphRun(new MockGlyphTypeface(), 10, characters, glyphIndices, glyphAdvances, - glyphClusters: glyphClusters, biDiLevel: bidiLevel); + return new GlyphRun(new MockGlyphTypeface(), 10, new string('a', count).AsMemory(), glyphInfos, bidiLevel); } } } diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 42e33729ac..93d97e6cfb 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; namespace Avalonia.Base.UnitTests.VisualTree { @@ -74,7 +75,7 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { throw new NotImplementedException(); } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index a272d89b8a..a802cd0958 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -5,6 +5,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Microsoft.Diagnostics.Runtime; namespace Avalonia.Benchmarks @@ -120,7 +121,7 @@ namespace Avalonia.Benchmarks return new MockStreamGeometryImpl(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return new MockGlyphRun(); } diff --git a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs index 31e485448e..772d6e1023 100644 --- a/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.RenderTests/Media/GlyphRunTests.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Documents; using Avalonia.Controls.Shapes; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Xunit; #if AVALONIA_SKIA @@ -170,11 +171,16 @@ namespace Avalonia.Direct2D1.RenderTests.Media var advance = glyphTypeface.GetGlyphAdvance(glyphIndices[0]) * scale; - var advances = new[] { advance, advance, advance}; + var glyphInfos = new[] + { + new GlyphInfo(glyphIndices[0], 0, advance), + new GlyphInfo(glyphIndices[1], 1, advance), + new GlyphInfo(glyphIndices[2], 2, advance) + }; var characters = new[] { 'A', 'B', 'C' }; - GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphIndices, advances); + GlyphRun = new GlyphRun(glyphTypeface, 100, characters, glyphInfos); } public GlyphRun GlyphRun { get; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index 04bc401479..f2d6670be5 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -134,11 +134,11 @@ namespace Avalonia.Skia.UnitTests.Media foreach (var rect in rects) { - var currentCluster = glyphRun.GlyphClusters[index]; + var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster; - while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphClusters.Count) + while (currentCluster == lastCluster && index + 1 < glyphRun.GlyphInfos.Count) { - currentCluster = glyphRun.GlyphClusters[++index]; + currentCluster = glyphRun.GlyphInfos[++index].GlyphCluster; } //Non trailing edge @@ -161,15 +161,15 @@ namespace Avalonia.Skia.UnitTests.Media var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Metrics.WidthIncludingTrailingWhitespace; - var rects = new List(glyphRun.GlyphAdvances!.Count); + var rects = new List(glyphRun.GlyphInfos!.Count); var lastCluster = -1; - for (var index = 0; index < glyphRun.GlyphAdvances.Count; index++) + for (var index = 0; index < glyphRun.GlyphInfos.Count; index++) { - var currentCluster = glyphRun.GlyphClusters![index]; + var currentCluster = glyphRun.GlyphInfos[index].GlyphCluster; - var advance = glyphRun.GlyphAdvances[index]; + var advance = glyphRun.GlyphInfos[index].GlyphAdvance; if (lastCluster != currentCluster) { @@ -216,10 +216,7 @@ namespace Avalonia.Skia.UnitTests.Media shapedBuffer.GlyphTypeface, shapedBuffer.FontRenderingEmSize, shapedBuffer.Text, - shapedBuffer.GlyphIndices, - shapedBuffer.GlyphAdvances, - shapedBuffer.GlyphOffsets, - shapedBuffer.GlyphClusters, + shapedBuffer.GlyphInfos, shapedBuffer.BidiLevel); if(shapedBuffer.BidiLevel == 1) @@ -233,7 +230,7 @@ namespace Avalonia.Skia.UnitTests.Media private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(), textShaperImpl: new TextShaperImpl(), fontManagerImpl: new CustomFontManagerImpl())); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs index b90752861c..6b9fb579b1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -585,7 +585,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var expectedRuns = expectedTextLine.TextRuns.Cast().ToList(); - var expectedGlyphs = expectedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); + var expectedGlyphs = expectedRuns + .SelectMany(run => run.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex) + .ToList(); for (var i = 0; i < text.Length; i++) { @@ -604,7 +606,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedRuns = textLine.TextRuns.Cast().ToList(); - var actualGlyphs = shapedRuns.SelectMany(x => x.GlyphRun.GlyphIndices).ToList(); + var actualGlyphs = shapedRuns + .SelectMany(x => x.GlyphRun.GlyphInfos, (_, glyph) => glyph.GlyphIndex) + .ToList(); Assert.Equal(expectedGlyphs, actualGlyphs); } @@ -706,7 +710,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), + .With(renderInterface: new PlatformRenderInterface(), textShaperImpl: new TextShaperImpl())); AvaloniaLocator.CurrentMutable diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index 1fd26748cd..a24a0fcf70 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -142,8 +142,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting black, textWrapping: TextWrapping.Wrap); - var expectedGlyphs = expected.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() - .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); + var expectedGlyphs = GetGlyphs(expected); var outer = new GraphemeEnumerator(text); var inner = new GraphemeEnumerator(text); @@ -175,8 +174,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting textWrapping: TextWrapping.Wrap, textStyleOverrides: spans); - var actualGlyphs = actual.TextLines.Select(x => string.Join('|', x.TextRuns.Cast() - .SelectMany(x => x.ShapedBuffer.GlyphIndices))).ToList(); + var actualGlyphs = GetGlyphs(actual); Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count); @@ -196,6 +194,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting i += outer.Current.Text.Length; } } + + static List GetGlyphs(TextLayout textLayout) + => textLayout.TextLines + .Select(line => string.Join('|', line.TextRuns + .Cast() + .SelectMany(run => run.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphIndex))) + .ToList(); } [Fact] @@ -484,13 +489,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - var glyphClusters = shapedRun.ShapedBuffer.GlyphClusters; + var glyphClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToArray(); - var expected = clusters.Skip(index).Take(glyphClusters.Count).ToArray(); + var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray(); Assert.Equal(expected, glyphClusters); - index += glyphClusters.Count; + index += glyphClusters.Length; } } } @@ -515,13 +520,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, layout.TextLines[0].TextRuns.Count); - Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Count); + Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphInfos.Count); - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[5]); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[5].GlyphCluster); if (expectedLength == 7) { - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphClusters[6]); + Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[6].GlyphCluster); } } } @@ -562,9 +567,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint); - foreach (var glyph in textRun.GlyphRun.GlyphIndices) + foreach (var glyphInfo in textRun.GlyphRun.GlyphInfos) { - Assert.Equal(replacementGlyph, glyph); + Assert.Equal(replacementGlyph, glyphInfo.GlyphIndex); } } } @@ -776,8 +781,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(textLine.WidthIncludingTrailingWhitespace, rect.Width); } - var rects = layout.TextLines.SelectMany(x => x.TextRuns.Cast()) - .SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToArray(); + var rects = layout.TextLines + .SelectMany(x => x.TextRuns.Cast()) + .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance) + .ToArray(); for (var i = 0; i < SingleLineText.Length; i++) { @@ -865,10 +872,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var currentX = 0.0; - for (var i = 0; i < firstRun.GlyphRun.GlyphClusters.Count; i++) + for (var i = 0; i < firstRun.GlyphRun.GlyphInfos.Count; i++) { - var cluster = firstRun.GlyphRun.GlyphClusters[i]; - var advance = firstRun.GlyphRun.GlyphAdvances[i]; + var cluster = firstRun.GlyphRun.GlyphInfos[i].GlyphCluster; + var advance = firstRun.GlyphRun.GlyphInfos[i].GlyphAdvance; hit = layout.HitTestPoint(new Point(currentX, 0)); @@ -895,10 +902,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting currentX = firstRun.Size.Width + 0.5; - for (var i = 0; i < secondRun.GlyphRun.GlyphClusters.Count; i++) + for (var i = 0; i < secondRun.GlyphRun.GlyphInfos.Count; i++) { - var cluster = secondRun.GlyphRun.GlyphClusters[i]; - var advance = secondRun.GlyphRun.GlyphAdvances[i]; + var cluster = secondRun.GlyphRun.GlyphInfos[i].GlyphCluster; + var advance = secondRun.GlyphRun.GlyphInfos[i].GlyphAdvance; hit = layout.HitTestPoint(new Point(currentX, 0)); @@ -932,7 +939,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var firstRun = (ShapedTextRun)textLine.TextRuns[0]; - var firstCluster = firstRun.ShapedBuffer.GlyphClusters[0]; + var firstCluster = firstRun.ShapedBuffer.GlyphInfos[0].GlyphCluster; var characterHit = textLine.GetCharacterHitFromDistance(0); @@ -946,7 +953,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); - var firstAdvance = firstRun.ShapedBuffer.GlyphAdvances[0]; + var firstAdvance = firstRun.ShapedBuffer.GlyphInfos[0].GlyphAdvance; Assert.Equal(firstAdvance, distance, 5); @@ -991,9 +998,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedRuns = textLine.TextRuns.Cast().ToList(); - var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphClusters).ToList(); + var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster).ToList(); - var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphAdvances).ToList(); + var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance).ToList(); var currentX = 0.0; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index 6993c70e8b..544b84912e 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -95,9 +95,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - clusters.AddRange(shapedRun.IsReversed ? - shapedRun.ShapedBuffer.GlyphClusters.Reverse() : - shapedRun.ShapedBuffer.GlyphClusters); + var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster); + + clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); @@ -142,9 +142,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedRun = (ShapedTextRun)textRun; - clusters.AddRange(shapedRun.IsReversed ? - shapedRun.ShapedBuffer.GlyphClusters.Reverse() : - shapedRun.ShapedBuffer.GlyphClusters); + var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster); + + clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters); } clusters.Reverse(); @@ -247,7 +247,9 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting formatter.FormatLine(textSource, 0, double.PositiveInfinity, new GenericTextParagraphProperties(defaultProperties)); - var clusters = textLine.TextRuns.Cast().SelectMany(x => x.ShapedBuffer.GlyphClusters) + var clusters = textLine.TextRuns + .Cast() + .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster) .ToArray(); var previousCharacterHit = new CharacterHit(text.Length); @@ -313,11 +315,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var glyphRun = textRun.GlyphRun; - for (var i = 0; i < glyphRun.GlyphClusters!.Count; i++) + for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { - var cluster = glyphRun.GlyphClusters[i]; + var cluster = glyphRun.GlyphInfos[i].GlyphCluster; - var advance = glyphRun.GlyphAdvances[i]; + var advance = glyphRun.GlyphInfos[i].GlyphAdvance; var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); @@ -750,7 +752,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedBuffer = textRun.ShapedBuffer; - var currentClusters = shapedBuffer.GlyphClusters.ToList(); + var currentClusters = shapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToList(); foreach (var currentCluster in currentClusters) { @@ -783,11 +785,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var shapedBuffer = textRun.ShapedBuffer; - for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++) + for (var index = 0; index < shapedBuffer.GlyphInfos.Length; index++) { - var currentCluster = shapedBuffer.GlyphClusters[index]; + var currentCluster = shapedBuffer.GlyphInfos[index].GlyphCluster; - var advance = shapedBuffer.GlyphAdvances[index]; + var advance = shapedBuffer.GlyphInfos[index].GlyphAdvance; if (lastCluster != currentCluster) { diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs index 834dce4a90..3f02867aa9 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextShaperTests.cs @@ -19,10 +19,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedBuffer = TextShaper.Current.ShapeText(text, options); Assert.Equal(shapedBuffer.Length, text.Length); - Assert.Equal(shapedBuffer.GlyphClusters.Count, text.Length); - Assert.Equal(0, shapedBuffer.GlyphClusters[0]); - Assert.Equal(1, shapedBuffer.GlyphClusters[1]); - Assert.Equal(1, shapedBuffer.GlyphClusters[2]); + Assert.Equal(shapedBuffer.GlyphInfos.Length, text.Length); + Assert.Equal(0, shapedBuffer.GlyphInfos[0].GlyphCluster); + Assert.Equal(1, shapedBuffer.GlyphInfos[1].GlyphCluster); + Assert.Equal(1, shapedBuffer.GlyphInfos[2].GlyphCluster); } } @@ -36,7 +36,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedBuffer = TextShaper.Current.ShapeText(text, options); Assert.Equal(shapedBuffer.Length, text.Length); - Assert.Equal(100, shapedBuffer.GlyphAdvances[0]); + Assert.Equal(100, shapedBuffer.GlyphInfos[0].GlyphAdvance); } } diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index f9e1e45098..d56e360e22 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -4,6 +4,7 @@ using System.IO; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Media.Imaging; +using Avalonia.Media.TextFormatting; using Avalonia.Rendering; using Moq; @@ -146,7 +147,7 @@ namespace Avalonia.UnitTests throw new NotImplementedException(); } - public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphIndices, IReadOnlyList glyphAdvances, IReadOnlyList glyphOffsets) + public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { return Mock.Of(); } diff --git a/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png b/tests/TestFiles/Skia/Media/GlyphRun/Should_Render_GlyphRun_Geometry.expected.png index 407b67b8a0d49afd3cd0f616c45c01d9fd480222..547e9b276380f69573a79583543ae3419d6a4c0f 100644 GIT binary patch literal 4222 zcmbuD_dgVlq-?gRwfx=G} zVod!{q13Q=5)uY!h`Ne#VE%rAfd}WTzw%hY1INLDG|6!)@ucQ585zSr{NLI@9#x3A z4~c>{rPxa9bU|xr6E{(a2Bk*0#^2_L+5@`c-%yw+QC1f+@6)U<Pj3O*xh#eu__8-l}SQ5Yg ze<3Ug+M_WO1G;M=U+{dWjptq|j^`lNsb*XuVOU{UnQFHEt463jed5GRaigI4u|(GM z&#lQzuicbJ-#{}z%W_V2S*?{#z;ZH&Y76nq4Ofc(^-P1EQ4cV8CEBq#3wXp1bSIE{ zawghp`jCD1Iq%qNxh9;L(^XaKdl}GDV%`*}`A-JXTOHuHW}W%+A7pS78FY0GI1+fo z3=0#zM{|}krN9!^x-nDCRlt=P<$cboM}Rf{+Xsh9yfF-z!k{h2JeN(&-4CanJaIUBgLV>jj^q$r9S z!yPBoecqDu7BIcXJPsJQ^1ENN+?fIgLX_CtFwmUB`z8pzto}2rm9LyvX``x9pFlZ$) z>^%NfLfg@WMQ=Sc{LmjvYA*K447mQEq9{QC{63OT5wZ0&OctGula^OfZ3pU|;=9g) zRi4R!?Wd-CN>*ANY;()!Xe7x0ns4{R6Z4LRZboz2g)b!*hDfXdpiA_)GJ(I9W% z^LC$rD4QSI`^CFWu<$t4`*9u{aXbe(>lKS3;p}T^?b~WFaI@gm1Y(+3{4HzbvAhMV z#8KRpWCO)~61%v4hwNt&hN&Ep{PAxL6gEC>ZagYI7X6Tb>uYKF&vIY#GqtWJXLfj5 zBTCyY7Dy#_!R_7on8xWgx@W&k03q1eFjBB3=PGbfPp?WHGn4OX@m-kJeA})os(~ue z^v>bZ2y)(D^o1L~6zw&_Wi%0J`;@V9FX58CCzjpFPH7Dgr9u}DeWHcM$uk?ZTR_cp zxBYF9&jx3!A~^Fj6K!dc;yN1dhW3WKD3oOUdY`+n5U#bEfLniXU-*42pD&f&7iNu| zFGb4Y=ndDhGuWvoR0fdm0`{(D;OWuLXf^pZ;m&^djrH>;Gn|*CTx0frI9u_u5CQ$_ zFJP%2f4OpxIQ8EtO2LAt2i5T@cSyNUjQEh8(EOH^<~rh-&dR~>=Nl}`;$j}p^^u|! zTk;}eLcnD za|QC)*q&Vzpt!NN>Rob>e;ZOR0i`tX@~qw4XsQNM+lp0O$jw@RKptN_;eKPldF%a`1-pKrPh6-|~LjvxU3+IO^Ru`OnHShO8;*s*eGw|p#TTFF^ZPfNAc{{hD zzoE`;BTetVrG;F_rA2#tIag^@?DK|$tPTo1>^l^@z&d`!1MS9lol-zr0|kEqD?dnU zC#!I~`#HbA%w-wXX;R*8o9r)i`fu#$Z&%&McCK9NaUm{cRd)q)|8X!HPnmALP20x! z73`FI9y23ZVQkt9)`qvyf}XUZqgqN`wcc4$+N4%x$Yf>*s5=O*Fql|mvM}WVUh}t= zazE<_r^vu9XZF|hiby>q=M|w>+fIEh{`{OaKA{yGZqJ0GyDzf&b;$1plkAdniazmW zx&l}dHKTINwky6(A-pE;eeUb@g)v_kXFFYkBEh<5;C-;D0Lzm8IFc=tSC5--d!()t z*UxzSHg-g6#fKZ`s?|IuZO@bwmdlhyACa7GXA{d0=IC^VZRP~JdG<>luxZKu>G@LD z$X-(A<=qvM)?fl6EDg7Is-_|Pz>rht)oM%zP2MyIr?&UTj@kSr?qOSvPv6J9)tD_g zxf9l5gEx+CtfhXH0&@MwZB_J!?snG zhSp0DkyMp&XCE_BKI5;z&mwROJ3&cNh5@Nf65X>wC~Qx)NX0Ile1=+V=HaMLL1G^? zI#wXMU2bu-Uf}b)K1K5A)~HO%`oiT{gwn?MCyp%r1mSGfT)Oafmn?M6z;C-<1*ID9 z4L4mT`xDA!L>Xq1h7>w3_jcx0Zgx_J!h;ibr^%O+g9}sdl0fi^^UXH|wYH`8dRb%4 z#En@cEKPi##=feut(RcC<}pZyiWWm3uf1gbEMv+z(A@{7xs%+)aFrTk9I#J<(YbNR zO@}nETWyUUWh)NU_33OlP=#G2)P<%czWh3r?HfiDo&;_O2elV&-49Bwu4W4HOJ}K{ zlmBtcuG2xG+hV%4xNC_T#IiX`hDV7X)E~*|eMu;khHy~_KFE7c-uUNh{-3efI_Zu< zx{bc4oxYVtr2{Rryq{E}s5sj(8IL2rRb!lU8ZvV}q%Y5t9p=3dBb`*BXC`wFmh}=C zFh0`9BXe<9m;Dr3=xqkO^yPrfLHm-tPBVXTN+BRR{($hVt=+iC*v`C4d}->t zWzkl17M_8^!vn&b(w@V<0y(W`*yp--NuWmNv36-*rtTYIC1@bx_a{kd`0`mEnbj*y z(U!dkFY~v?^!`A8?MW}j0iCs!D1Af^X9yo3=vBGMN_p5W+bIXDHBkI_fU`*Jjl1op z%a<1w&2aATS_=W~i-x%2%N9laU;N_hFG@p{a?R??iW;q6*@rkX_Pl0zyqma=pf24j zc%avBma99Me)SCN-;US(P((;BzB>Kj&H)KLrQjuZ@nH8)0gHG00*+QcxeqYEGuoRW zJ$xlR9b+8UuVYkMf6MsO(>2EaP0omUr|^_d(A-V1wXm!@Ewd5q)Wnq!7CCuv!9HuM zBjxwQkm5`$Q9N35eeUiPhn+=9m5_&gc5Qar^y|gi|5}1h$)&ZOy%E!Y*!p8!m!uwY zP(1KVs+a+nWOHUKt)DYRlj6RHXa&~hhns#Yd5Q2sOBnAsC%sXpazNlm#cl}=e2UNz zi4z~Ys7vZ)`s~~dwnBrRQzMxxdOyPGN4TUdrM(zW*Rcr?*@8WK{R}fHxdavHI?S(z zZGYsyuA~8s6dVrLSUO>)EwZLhiD#|C4`6K_`^kE$%b8<3xqr z7TspQ67RHG!d8fo1Kl5-8neVQ17gKZc#dFQT52S(6MCe;eA9~e705i(aIU?tq!BO4 zEQ|~aT=jO;pcTDRa0|^45xV?C_+q;{$!*?UTpDyLYmb{?q!bmi9PG0%RC)vCRyWs8< z4Y1}FO|vO*Rm)j^hPLdl5#6M)3x=zT#$6Ek@O=&&TfNe7sCPo__qz|G z6COUpV(N~6q2mqXgyKcF7gSL7q3PUPW~iM!=xX2OgXZ@!Gny^$tD~nVav?+WOT;c`QhYxKX8L5zh@x@FCM^rw-^nBj`TH=Dt&0i#M%!8~&+Mr)k;w zHG$J=In$FhZ2JV>LiF@1+vmG08Ar+IS-QPTQO~o6hiA-QzSy8a75m6GLDFlv;LaIISh5#3JS*cv=77M}$AXBfaqkujq7{$Yc6ic)Fo>NeM_e!uL*Qu0MWmZYg zVOzMaX#MT8R8u)cq$L^UR1;J&JZJNDnAgxQ*nS-Lt(Iq_VN6guQO%#7?V?$MkLc?C@G!9B4|qU?mY#;t!(A1SIb zxQ|eBWhoV4V(<;=Wxt$qK;hl@qtn#g*rMb))YyUc!9qyx8Ybf*2x%v z?ju@28OIktww&-ZrDN+=b=kc^lJ*fV6KG(s(MjEo2vI_UC6pBV^l zPcGq}FiMbLb1tm*XSs2lCmS#(V*U7<`*6{-*@x@Iw+fHnd9`=?m*B8-A*T;TFOBkH zNqYS9fo&~f0O?BhUcxA4iu5_jEZHq5!}ZIT%3x+y4R@XutpyWzn_BhsNq~iz+yM#k zGNJWjks|G?)WK~=y<+A38TEK?cE+jJETEzX+09#bvGxDk>V|x~A&*d7Cy9!yIl7Sdso@G zEHFzuBdh&~K^ppH`=^)1fq_eWnqMA#D`)h4odK%rYAFbo5()Ib# zDukn7jP?&Vh@kf8gl@T*H*{Ckj!NhL+bdOn2xC%AIsRG}c9kZJM%OjdCTPBI6hcQ6 zU7+WQHwIZSS*HTrC$~=(aO^AQp8B;}c~*2+>GIqy;lVFUmqGn!o!o})=@-Zd8R?bl zY}cHbV_wGCXu#yIJf4k|qqAq~&@UEG&g--ed1|dx+dWf& z&m_~-W1-P%QLjF3*GfjHY+RAGe|sj@&8y{>Y-v{^n^Mi4}mB8K~ z4QHTzRdpPq+^N{J$KRvF*466-$-H2bW(aM<3(HuT+Qlk5N)0I9$NZ@#Ga$N5BR|k( zHxFJ2*C_1r4G9Zqz5Yt$E4-D(y5mQM*YmN7U@3c1sIO2`u4g%vUKmxNS3X}^DuY&K zu&?;J^^+aaUFp(lGbATYn58Ah3-gOn$;z7Pm`#e21@l;8j)u)Y3QhEfWKOmh51Ho~ z`4=X30TiTU3iygIL^{)XE|_Teai7oAme%8phB;P_}B8lVS1v z=7M@vnsf4}gBqkOUT+GvkNuZvG5LM)ZtEd`J$(1sU}=lN7`w(1JwIUAGK9+A{;9*k z-Ox@hAEu^)S`N3`Lzypq2;z2#@UvgW1xdxpj7cDWEJCv{+l^i2_kh43BPp$@;1|*O zW@yv66Kwl-gtJIoTjSHbsnxYVHtv@DBRYOc@)AU_q>iqDIha~2(DwPVdZ{PcX1(~1 z*r(KBA?{sPpv6lJuyy9`bg#{%?4RxIXr*H9Aa7aMT==1?AD1r~w}IP?sCzWdxl z-i!zY?+}L6i~zqcG6*8f0XsUZQcu75>FZ@tujffHY4 zacn+QRFlR#rxbsSt$(MsvI^O`YB(|E;)uTB;J?kO#lkvL5rY))`%Dz66 z`Ju$bCd%wf9f?^%h6n>hn&Hg+r2F<(45IDSeognKOxsqD>-s%Eu+~hQN3Z*G70>Zh zB}hfS0dqH=2cSdvK6}#Lo{8Q}h9k&nuMnYmT@XxuB@U4+M{7=LJdVA`Mre8C<*EA| zz`0Ta5`+sYe1;&$kGIkg&Q1{ivPqoEH|E|sPLN0f&4yHpsU`eIZ!N~t_OF*xT|xs? zX(Jr%6PIP@5Eg5yB=HoQ8!gtx^@lfh)M1f?V@cvGu%}hL(t>X8G^4vz)bJ{ie} zdk_D5+gAlEZNT4z$mW z=>$|%%bu92iF1F)&?4t?Ou&M!#1a5qZH&gy(wc{5017=(JFYGsrT3=9Zr|A4#4gp` z%@_2vcw%hnwPs}F`O)BuQDQ@!Cg|K${A7vmWD4r9B(K|AUM9!~6B(Y)as3S$mAG8$w3syjF`91VAsR8kk7F&@7qtc z43;9!RJ=o8PU?7g>t^)Tir34^Mrhi?_Eq)Riw;UvG9RbEEj(xB&5-du zb=s*h+Z~u1dmao)dN7I07>=G)ggTJ7g=WQo3L96oYN!Bci9jXH@&x{!Pw2dsgG7XA zy_3M%E=trL@fgDIsG)ED<4?~s!8QT^0_=jitOe}Ls%ncA^Vb3MPS*ltu{4LnPT~;n zR!M8aU$N^jW4RqVPV8z`Qa{k$F8D3sVVMm7Ve|Kd-L3bm=t!c|kinAxNK(If%mQ!K zrphS|RJ3n;;+8l7AB6QFnLz9G-MPSir~oGb7%HdhKLce^9x*1bI!@>1SKRTjGI*6# zEPJ4(nO=Bdkobns`$_a%O80|Hbjj=a<$GS)(j%Yd06HFKVft+^NA1t*XC?g2Q;ffC zow~b*>we2aVCf(LHQ$j!>dBUj~ubO}v(a0^T#l3s*%hMsQ$=1?8 z`5fM&c4@TgKVVDwWKeVg)M#^>KITVc{_@@MTQl&CMq(qGVtdC^<{{ilVuM6d3SVge+^Pj7vNW--AJb$vas zX2YpWD;E(b$ z>uS<|it#6?#l0wkM(49b5|-XhV?VCW98qI5MHKKOe06Xn@Jw`8hkavJe}`1(>(8DA z>55be`P=i##QF-Lax`D8@*S+2q8JF!9dm^CRZX86|ajl(g%J zStfco&0FL%`K<61;T+&ap78Wo)U~dG2>~RkzcSURR~VN1^MQ%v{61-MByGB5h{{jI zv9B?MPvf#PyqWpFik^Vn)x@5d$0!rOnZ}RT{z+HH4xq(hg#yKGcPUxLSlw zJxlgOkYDpDrFiB-PCr33~sUx5ZgozNAR=Vb9}a z4p9{c^Dek3L<>gfTei%$phyuo#ZR+wyEM^4&L1e{K$24KJd2`YyZj+y@799^cUQyG zASaI3or2y1@)M!^u3q3q6r+>N;@HU(6CQ_CLk}XsBLLl0eJ*sN__=ZPoGP!YO~kIB=W zvcD0pe0_S*P*m^W4K=mHPhv}O>rjcArEo|_i-mDL_)|(FE9zQ5BT(9`E2X(jcc$qm z2vqOl#+}{kJUPm|v6K`$=jpxqLt+Gv+`jJvxxy+qW0ff>iMFvPziw7X5_-0*wwCYc zl~ee_v`VA@I?N7~XxoYFV7?J`9%H#~h}0M|TA+7mbZf@mf3IA9T%k5qXy$EhD--5h z$-fXu!FG?`klwu~U*a3uxPzM=Up}^hRbDW&dTudkynXjkGw($}d^V_^QcEJ8K_Jb= z?46&tL^D%-Zqo4H9P1Fl%d0f2;eQWylFrje6?grNI+Rh|3>GHd2wU0&xujz0XD;cf zLo>oY2~q&M!9)GO;HLifkBkJdFGEg#qe+EkIAzA5jTe-oUN}XgY6?ynChbkgHk{(= zv6$twf^P}k0wG~ZFAiYM(o`mipAH&rDC0r|-MFS_4M1qT#I;!ZZV420$Xa(0H@WdCxf(Bd^&gsfPNv z(PUMiZB+Xr!)pj5!Nb4U%f*RCM_=z&6tR64`yu|bgsRVrdzwAQlIf3@e7{_LM7{O Date: Wed, 18 Jan 2023 12:48:46 +0100 Subject: [PATCH 11/23] Added FormattingObjectPool to reduce temp allocs during text layout --- .../Media/TextFormatting/BidiReorderer.cs | 5 + .../TextFormatting/FormattingObjectPool.cs | 135 ++++++++++++++++ .../TextFormatting/InterWordJustification.cs | 3 +- .../Media/TextFormatting/TextCharacters.cs | 4 +- .../TextFormatting/TextEllipsisHelper.cs | 17 +- .../Media/TextFormatting/TextFormatterImpl.cs | 149 +++++++++++------- .../Media/TextFormatting/TextLayout.cs | 25 ++- .../TextLeadingPrefixCharacterEllipsis.cs | 90 ++++++----- .../Media/TextFormatting/TextLineImpl.cs | 26 ++- src/Avalonia.Base/Utilities/ArrayBuilder.cs | 36 ++++- src/Avalonia.Base/Utilities/ArraySlice.cs | 1 - .../Avalonia.UnitTests/MockTextShaperImpl.cs | 5 +- 12 files changed, 359 insertions(+), 137 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs index 3fcb7bf420..2c6db4b753 100644 --- a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -10,9 +10,14 @@ namespace Avalonia.Media.TextFormatting /// To avoid allocations, this class is designed to be reused. internal sealed class BidiReorderer { + [ThreadStatic] private static BidiReorderer? t_instance; + private ArrayBuilder _runs; private ArrayBuilder _ranges; + public static BidiReorderer Instance + => t_instance ??= new(); + public void BidiReorder(Span textRuns, FlowDirection flowDirection) { Debug.Assert(_runs.Length == 0); diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs new file mode 100644 index 0000000000..0468d8f413 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Contains various list pools that are commonly used during text layout. + /// + /// This class provides an instance per thread. + /// In most applications, there'll be only one instance: on the UI thread, which is responsible for layout. + /// + /// + /// + internal sealed class FormattingObjectPool + { + [ThreadStatic] private static FormattingObjectPool? t_instance; + + /// + /// Gets an instance of this class for the current thread. + /// + /// + /// Since this is backed by a thread static field which is slower than a normal static field, + /// prefer passing the instance around when possible instead of calling this property each time. + /// + public static FormattingObjectPool Instance + => t_instance ??= new(); + + public ListPool TextRunLists { get; } = new(); + + public ListPool UnshapedTextRunLists { get; } = new(); + + public ListPool TextLines { get; } = new(); + + [Conditional("DEBUG")] + public void VerifyAllReturned() + { + TextRunLists.VerifyAllReturned(); + UnshapedTextRunLists.VerifyAllReturned(); + TextLines.VerifyAllReturned(); + } + + internal sealed class ListPool + { + // we don't need a big number here, these are for temporary usages only which should quickly be returned + private const int MaxSize = 16; + + private readonly RentedList[] _lists = new RentedList[MaxSize]; + private int _size; + private int _pendingReturnCount; + + /// + /// Rents a list. + /// See for the intended usages. + /// + /// A rented list instance that must be returned to the pool. + /// + public RentedList Rent() + { + var list = _size > 0 ? _lists[--_size] : new RentedList(); + + Debug.Assert(list.Count == 0, "A RentedList has been used after being returned!"); + + ++_pendingReturnCount; + return list; + } + + /// + /// Returns a rented list to the pool. + /// + /// + /// On input, the list to return. + /// On output, the reference is set to null to avoid misuse. + /// + public void Return(ref RentedList? rentedList) + { + if (rentedList is null) + { + return; + } + + --_pendingReturnCount; + rentedList.Clear(); + + if (_size < MaxSize) + { + _lists[_size++] = rentedList; + } + + rentedList = null; + } + + [Conditional("DEBUG")] + public void VerifyAllReturned() + { + if (_pendingReturnCount > 0) + { + throw new InvalidOperationException( + $"{_pendingReturnCount} RentedList<{typeof(T).Name} haven't been returned to the pool!"); + } + + if (_pendingReturnCount < 0) + { + throw new InvalidOperationException( + $"{-_pendingReturnCount} RentedList<{typeof(T).Name} extra lists have been returned to the pool!"); + } + } + } + + /// + /// Represents a list that has been rented through . + /// + /// This class can be used when a temporary list is needed to store some items during text layout. + /// It can also be used as a reusable array builder by calling when done. + /// + /// + /// NEVER use an instance of this type after it's been returned to the pool. + /// AVOID storing an instance of this type into a field or property. + /// AVOID casting an instance of this type to another type. + /// + /// AVOID passing an instance of this type as an argument to a method expecting a standard list, + /// unless you're absolutely sure it won't store it. + /// + /// + /// If you call a method returning an instance of this type, + /// you're now responsible for returning it to the pool. + /// + /// + /// + /// The type of elements in the list. + internal sealed class RentedList : List + { + } + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs index 6bfcfc06f8..7afb758038 100644 --- a/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs +++ b/src/Avalonia.Base/Media/TextFormatting/InterWordJustification.cs @@ -48,8 +48,9 @@ namespace Avalonia.Media.TextFormatting var currentPosition = textLine.FirstTextSourceIndex; - foreach (var textRun in lineImpl.TextRuns) + for (var i = 0; i < lineImpl.TextRuns.Count; ++i) { + var textRun = lineImpl.TextRuns[i]; var text = textRun.Text; if (text.IsEmpty) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs index 6454f9bfa3..c1f3816e54 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextCharacters.cs @@ -1,6 +1,6 @@ using System; -using System.Collections.Generic; using Avalonia.Media.TextFormatting.Unicode; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { @@ -47,7 +47,7 @@ namespace Avalonia.Media.TextFormatting /// /// The shapeable text characters. internal void GetShapeableCharacters(ReadOnlyMemory text, sbyte biDiLevel, - ref TextRunProperties? previousProperties, List results) + ref TextRunProperties? previousProperties, RentedList results) { var properties = Properties; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs index 97f8b2483b..e6743f5533 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextEllipsisHelper.cs @@ -1,8 +1,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -111,20 +109,17 @@ namespace Avalonia.Media.TextFormatting return new[] { shapedSymbol }; } - // perf note: the runs are very likely to come from TextLineImpl - // which already uses an array: ToArray() won't ever be called in this case - var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); + var objectPool = FormattingObjectPool.Instance; - var (preSplitRuns, _) = TextFormatterImpl.SplitTextRuns(textRunArray, collapsedLength); + var (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns(textRuns, collapsedLength, objectPool); var collapsedRuns = new TextRun[preSplitRuns.Count + 1]; + preSplitRuns.CopyTo(collapsedRuns); + collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; - for (var i = 0; i < preSplitRuns.Count; ++i) - { - collapsedRuns[i] = preSplitRuns[i]; - } + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); - collapsedRuns[collapsedRuns.Length - 1] = shapedSymbol; return collapsedRuns; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 8ffe3e5da2..c25d530472 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -1,14 +1,16 @@ -using System; +// ReSharper disable ForCanBeConvertedToForeach +using System; using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Utilities; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { - internal class TextFormatterImpl : TextFormatter + internal sealed class TextFormatterImpl : TextFormatter { private static readonly char[] s_empty = { ' ' }; private static readonly char[] s_defaultText = new char[TextRun.DefaultTextSourceLength]; @@ -23,20 +25,25 @@ namespace Avalonia.Media.TextFormatting var textWrapping = paragraphProperties.TextWrapping; FlowDirection resolvedFlowDirection; TextLineBreak? nextLineBreak = null; - IReadOnlyList textRuns; + IReadOnlyList? textRuns; + var objectPool = FormattingObjectPool.Instance; - var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, + var fetchedRuns = FetchTextRuns(textSource, firstTextSourceIndex, objectPool, out var textEndOfLine, out var textSourceLength); + RentedList? shapedTextRuns; + if (previousLineBreak?.RemainingRuns is { } remainingRuns) { resolvedFlowDirection = previousLineBreak.FlowDirection; textRuns = remainingRuns; nextLineBreak = previousLineBreak; + shapedTextRuns = null; } else { - textRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, out resolvedFlowDirection); + shapedTextRuns = ShapeTextRuns(fetchedRuns, paragraphProperties, objectPool, out resolvedFlowDirection); + textRuns = shapedTextRuns; if (nextLineBreak == null && textEndOfLine != null) { @@ -49,25 +56,32 @@ namespace Avalonia.Media.TextFormatting switch (textWrapping) { case TextWrapping.NoWrap: - { - textLine = new TextLineImpl(textRuns.ToArray(), firstTextSourceIndex, textSourceLength, - paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); + { + // perf note: if textRuns comes from remainingRuns above, it's very likely coming from this class + // which already uses an array: ToArray() won't ever be called in this case + var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); - textLine.FinalizeLine(); + textLine = new TextLineImpl(textRunArray, firstTextSourceIndex, textSourceLength, + paragraphWidth, paragraphProperties, resolvedFlowDirection, nextLineBreak); - break; - } + textLine.FinalizeLine(); + + break; + } case TextWrapping.WrapWithOverflow: case TextWrapping.Wrap: - { - textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, paragraphProperties, - resolvedFlowDirection, nextLineBreak); - break; - } + { + textLine = PerformTextWrapping(textRuns, firstTextSourceIndex, paragraphWidth, + paragraphProperties, resolvedFlowDirection, nextLineBreak, objectPool); + break; + } default: throw new ArgumentOutOfRangeException(nameof(textWrapping)); } + objectPool.TextRunLists.Return(ref shapedTextRuns); + objectPool.TextRunLists.Return(ref fetchedRuns); + return textLine; } @@ -76,9 +90,12 @@ namespace Avalonia.Media.TextFormatting /// /// The text run's. /// The length to split at. + /// A pool used to get reusable formatting objects. /// The split text runs. - internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length) + internal static SplitResult> SplitTextRuns(IReadOnlyList textRuns, int length, + FormattingObjectPool objectPool) { + var first = objectPool.TextRunLists.Rent(); var currentLength = 0; for (var i = 0; i < textRuns.Count; i++) @@ -94,8 +111,6 @@ namespace Avalonia.Media.TextFormatting var firstCount = currentRun.Length >= 1 ? i + 1 : i; - var first = new List(firstCount); - if (firstCount > 1) { for (var j = 0; j < i; j++) @@ -108,7 +123,7 @@ namespace Avalonia.Media.TextFormatting if (currentLength + currentRun.Length == length) { - var second = secondCount > 0 ? new List(secondCount) : null; + var second = secondCount > 0 ? objectPool.TextRunLists.Rent() : null; if (second != null) { @@ -122,13 +137,13 @@ namespace Avalonia.Media.TextFormatting first.Add(currentRun); - return new SplitResult>(first, second); + return new SplitResult>(first, second); } else { secondCount++; - var second = new List(secondCount); + var second = objectPool.TextRunLists.Rent(); if (currentRun is ShapedTextRun shapedTextCharacters) { @@ -144,11 +159,16 @@ namespace Avalonia.Media.TextFormatting second.Add(textRuns[i + j]); } - return new SplitResult>(first, second); + return new SplitResult>(first, second); } } - return new SplitResult>(textRuns, null); + for (var i = 0; i < textRuns.Count; i++) + { + first.Add(textRuns[i]); + } + + return new SplitResult>(first, null); } /// @@ -157,14 +177,16 @@ namespace Avalonia.Media.TextFormatting /// The text runs to shape. /// The default paragraph properties. /// The resolved flow direction. + /// A pool used to get reusable formatting objects. /// /// A list of shaped text characters. /// - private static List ShapeTextRuns(List textRuns, TextParagraphProperties paragraphProperties, + private static RentedList ShapeTextRuns(IReadOnlyList textRuns, + TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool, out FlowDirection resolvedFlowDirection) { var flowDirection = paragraphProperties.FlowDirection; - var shapedRuns = new List(); + var shapedRuns = objectPool.TextRunLists.Rent(); if (textRuns.Count == 0) { @@ -172,13 +194,14 @@ namespace Avalonia.Media.TextFormatting return shapedRuns; } - - var bidiData = t_bidiData ??= new BidiData(); + var bidiData = t_bidiData ??= new(); bidiData.Reset(); bidiData.ParagraphEmbeddingLevel = (sbyte)flowDirection; - foreach (var textRun in textRuns) + for (var i = 0; i < textRuns.Count; ++i) { + var textRun = textRuns[i]; + ReadOnlySpan text; if (!textRun.Text.IsEmpty) text = textRun.Text.Span; @@ -190,8 +213,7 @@ namespace Avalonia.Media.TextFormatting bidiData.Append(text); } - var bidiAlgorithm = t_bidiAlgorithm ??= new BidiAlgorithm(); - + var bidiAlgorithm = t_bidiAlgorithm ??= new(); bidiAlgorithm.Process(bidiData); var resolvedEmbeddingLevel = bidiAlgorithm.ResolveEmbeddingLevel(bidiData.Classes); @@ -199,9 +221,11 @@ namespace Avalonia.Media.TextFormatting resolvedFlowDirection = (resolvedEmbeddingLevel & 1) == 0 ? FlowDirection.LeftToRight : FlowDirection.RightToLeft; - var processedRuns = new List(textRuns.Count); + var processedRuns = objectPool.TextRunLists.Rent(); + + CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); - CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels, processedRuns); + var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); for (var index = 0; index < processedRuns.Count; index++) { @@ -210,8 +234,9 @@ namespace Avalonia.Media.TextFormatting switch (currentRun) { case UnshapedTextRun shapeableRun: - { - var groupedRuns = new List(2) { shapeableRun }; + { + groupedRuns.Clear(); + groupedRuns.Add(shapeableRun); var text = shapeableRun.Text; while (index + 1 < processedRuns.Count) @@ -253,6 +278,9 @@ namespace Avalonia.Media.TextFormatting } } + objectPool.TextRunLists.Return(ref processedRuns); + objectPool.UnshapedTextRunLists.Return(ref groupedRuns); + return shapedRuns; } @@ -319,14 +347,13 @@ namespace Avalonia.Media.TextFormatting } } - private static bool CanShapeTogether(TextRunProperties x, TextRunProperties y) => MathUtilities.AreClose(x.FontRenderingEmSize, y.FontRenderingEmSize) && x.Typeface == y.Typeface && x.BaselineAlignment == y.BaselineAlignment; private static void ShapeTogether(IReadOnlyList textRuns, ReadOnlyMemory text, - TextShaperOptions options, List results) + TextShaperOptions options, RentedList results) { var shapedBuffer = TextShaper.Current.ShapeText(text, options); @@ -349,8 +376,8 @@ namespace Avalonia.Media.TextFormatting /// The bidi levels. /// A list that will be filled with the processed runs. /// - private static void CoalesceLevels(IReadOnlyList textCharacters, ArraySlice levels, - List processedRuns) + private static void CoalesceLevels(IReadOnlyList textCharacters, ReadOnlySpan levels, + RentedList processedRuns) { if (levels.Length == 0) { @@ -437,19 +464,20 @@ namespace Avalonia.Media.TextFormatting /// /// The text source. /// The first text source index. + /// A pool used to get reusable formatting objects. /// On return, the end of line, if any. /// On return, the processed text source length. /// /// The formatted text runs. /// - private static List FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, - out TextEndOfLine? endOfLine, out int textSourceLength) + private static RentedList FetchTextRuns(ITextSource textSource, int firstTextSourceIndex, + FormattingObjectPool objectPool, out TextEndOfLine? endOfLine, out int textSourceLength) { textSourceLength = 0; endOfLine = null; - var textRuns = new List(); + var textRuns = objectPool.TextRunLists.Rent(); var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); @@ -543,8 +571,10 @@ namespace Avalonia.Media.TextFormatting measuredLength = 0; var currentWidth = 0.0; - foreach (var currentRun in textRuns) + for (var i = 0; i < textRuns.Count; ++i) { + var currentRun = textRuns[i]; + switch (currentRun) { case ShapedTextRun shapedTextCharacters: @@ -554,15 +584,15 @@ namespace Avalonia.Media.TextFormatting var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphInfos[0].GlyphCluster; var lastCluster = firstCluster; - for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) + for (var j = 0; j < shapedTextCharacters.ShapedBuffer.Length; j++) { - var glyphInfo = shapedTextCharacters.ShapedBuffer[i]; + var glyphInfo = shapedTextCharacters.ShapedBuffer[j]; if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) { measuredLength += Math.Max(0, lastCluster - firstCluster); - goto found; + return measuredLength != 0; } lastCluster = glyphInfo.GlyphCluster; @@ -579,7 +609,7 @@ namespace Avalonia.Media.TextFormatting { if (currentWidth + drawableTextRun.Size.Width >= paragraphWidth) { - goto found; + return measuredLength != 0; } measuredLength += currentRun.Length; @@ -596,8 +626,6 @@ namespace Avalonia.Media.TextFormatting } } - found: - return measuredLength != 0; } @@ -605,7 +633,8 @@ namespace Avalonia.Media.TextFormatting /// Creates an empty text line. /// /// The empty text line. - public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties) + public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, double paragraphWidth, + TextParagraphProperties paragraphProperties, FormattingObjectPool objectPool) { var flowDirection = paragraphProperties.FlowDirection; var properties = paragraphProperties.DefaultTextRunProperties; @@ -618,7 +647,9 @@ namespace Avalonia.Media.TextFormatting var textRuns = new TextRun[] { new ShapedTextRun(shapedBuffer, properties) }; - return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection).FinalizeLine(); + var line = new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection); + line.FinalizeLine(); + return line; } /// @@ -630,14 +661,15 @@ namespace Avalonia.Media.TextFormatting /// The text paragraph properties. /// /// The current line break if the line was explicitly broken. + /// A pool used to get reusable formatting objects. /// The wrapped text line. private static TextLineImpl PerformTextWrapping(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection resolvedFlowDirection, - TextLineBreak? currentLineBreak) + TextLineBreak? currentLineBreak, FormattingObjectPool objectPool) { if (textRuns.Count == 0) { - return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties); + return CreateEmptyTextLine(firstTextSourceIndex, paragraphWidth, paragraphProperties, objectPool); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) @@ -763,10 +795,10 @@ namespace Avalonia.Media.TextFormatting break; } - var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength); + var (preSplitRuns, postSplitRuns) = SplitTextRuns(textRuns, measuredLength, objectPool); var lineBreak = postSplitRuns?.Count > 0 ? - new TextLineBreak(null, resolvedFlowDirection, postSplitRuns) : + new TextLineBreak(null, resolvedFlowDirection, postSplitRuns.ToArray()) : null; if (lineBreak is null && currentLineBreak?.TextEndOfLine != null) @@ -778,7 +810,12 @@ namespace Avalonia.Media.TextFormatting paragraphWidth, paragraphProperties, resolvedFlowDirection, lineBreak); - return textLine.FinalizeLine(); + textLine.FinalizeLine(); + + objectPool.TextRunLists.Return(ref preSplitRuns); + objectPool.TextRunLists.Return(ref postSplitRuns); + + return textLine; } private struct TextRunEnumerator diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index 55b6f14267..7a74dc89ae 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -426,16 +426,19 @@ namespace Avalonia.Media.TextFormatting private TextLine[] CreateTextLines() { + var objectPool = FormattingObjectPool.Instance; + if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, double.PositiveInfinity, _paragraphProperties, + FormattingObjectPool.Instance); Bounds = new Rect(0, 0, 0, textLine.Height); return new TextLine[] { textLine }; } - var textLines = new List(); + var textLines = objectPool.TextLines.Rent(); double left = double.PositiveInfinity, width = 0.0, height = 0.0; @@ -447,14 +450,15 @@ namespace Avalonia.Media.TextFormatting while (true) { - var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, - _paragraphProperties, previousLine?.TextLineBreak); + var textLine = textFormatter.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, + previousLine?.TextLineBreak); if (textLine.Length == 0) { if (previousLine != null && previousLine.NewLineLength > 0) { - var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, _paragraphProperties); + var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, MaxWidth, + _paragraphProperties, objectPool); textLines.Add(emptyTextLine); @@ -496,7 +500,7 @@ namespace Avalonia.Media.TextFormatting //Fulfill max lines constraint if (MaxLines > 0 && textLines.Count >= MaxLines) { - if(textLine.TextLineBreak is TextLineBreak lineBreak && lineBreak.RemainingRuns != null) + if(textLine.TextLineBreak?.RemainingRuns is not null) { textLines[textLines.Count - 1] = textLine.Collapse(GetCollapsingProperties(width)); } @@ -513,7 +517,7 @@ namespace Avalonia.Media.TextFormatting //Make sure the TextLayout always contains at least on empty line if (textLines.Count == 0) { - var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, MaxWidth, _paragraphProperties, objectPool); textLines.Add(textLine); @@ -552,7 +556,12 @@ namespace Avalonia.Media.TextFormatting } } - return textLines.ToArray(); + var result = textLines.ToArray(); + + objectPool.TextLines.Return(ref textLines); + objectPool.VerifyAllReturned(); + + return result; } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs index 672a15b398..0d777ad043 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLeadingPrefixCharacterEllipsis.cs @@ -1,7 +1,7 @@ // ReSharper disable ForCanBeConvertedToForeach using System; using System.Collections.Generic; -using System.Linq; +using static Avalonia.Media.TextFormatting.FormattingObjectPool; namespace Avalonia.Media.TextFormatting { @@ -80,60 +80,64 @@ namespace Avalonia.Media.TextFormatting if (measuredLength > 0) { - var collapsedRuns = new List(textRuns.Count + 1); + var objectPool = FormattingObjectPool.Instance; - // perf note: the runs are very likely to come from TextLineImpl, - // which already uses an array: ToArray() won't ever be called in this case - var textRunArray = textRuns as TextRun[] ?? textRuns.ToArray(); + var collapsedRuns = objectPool.TextRunLists.Rent(); - IReadOnlyList? preSplitRuns; - IReadOnlyList? postSplitRuns; + RentedList? rentedPreSplitRuns = null; + RentedList? rentedPostSplitRuns = null; + TextRun[]? results; - if (_prefixLength > 0) + try { - (preSplitRuns, postSplitRuns) = TextFormatterImpl.SplitTextRuns( - textRunArray, Math.Min(_prefixLength, measuredLength)); + IReadOnlyList? effectivePostSplitRuns; - for (var i = 0; i < preSplitRuns.Count; i++) + if (_prefixLength > 0) { - var preSplitRun = preSplitRuns[i]; - collapsedRuns.Add(preSplitRun); + (rentedPreSplitRuns, rentedPostSplitRuns) = TextFormatterImpl.SplitTextRuns( + textRuns, Math.Min(_prefixLength, measuredLength), objectPool); + + effectivePostSplitRuns = rentedPostSplitRuns; + + foreach (var preSplitRun in rentedPreSplitRuns) + { + collapsedRuns.Add(preSplitRun); + } + } + else + { + effectivePostSplitRuns = textRuns; } - } - else - { - preSplitRuns = null; - postSplitRuns = textRunArray; - } - collapsedRuns.Add(shapedSymbol); + collapsedRuns.Add(shapedSymbol); - if (measuredLength <= _prefixLength || postSplitRuns is null) - { - return collapsedRuns.ToArray(); - } + if (measuredLength <= _prefixLength || effectivePostSplitRuns is null) + { + results = collapsedRuns.ToArray(); + objectPool.TextRunLists.Return(ref collapsedRuns); + return results; + } - var availableSuffixWidth = availableWidth; + var availableSuffixWidth = availableWidth; - if (preSplitRuns is not null) - { - for (var i = 0; i < preSplitRuns.Count; i++) + if (rentedPreSplitRuns is not null) { - var run = preSplitRuns[i]; - if (run is DrawableTextRun drawableTextRun) + foreach (var run in rentedPreSplitRuns) { - availableSuffixWidth -= drawableTextRun.Size.Width; + if (run is DrawableTextRun drawableTextRun) + { + availableSuffixWidth -= drawableTextRun.Size.Width; + } } } - } - - for (var i = postSplitRuns.Count - 1; i >= 0; i--) - { - var run = postSplitRuns[i]; - switch (run) + for (var i = effectivePostSplitRuns.Count - 1; i >= 0; i--) { - case ShapedTextRun endShapedRun: + var run = effectivePostSplitRuns[i]; + + switch (run) + { + case ShapedTextRun endShapedRun: { if (endShapedRun.TryMeasureCharactersBackwards(availableSuffixWidth, out var suffixCount, out var suffixWidth)) @@ -151,10 +155,18 @@ namespace Avalonia.Media.TextFormatting break; } + } } } + finally + { + objectPool.TextRunLists.Return(ref rentedPreSplitRuns); + objectPool.TextRunLists.Return(ref rentedPostSplitRuns); + } - return collapsedRuns.ToArray(); + results = collapsedRuns.ToArray(); + objectPool.TextRunLists.Return(ref collapsedRuns); + return results; } return new TextRun[] { shapedSymbol }; diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 7fa9155b02..245104f8fe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1,14 +1,11 @@ using System; using System.Collections.Generic; -using System.Threading; using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { internal sealed class TextLineImpl : TextLine { - private static readonly ThreadLocal s_bidiReorderer = new(() => new BidiReorderer()); - private readonly TextRun[] _textRuns; private readonly double _paragraphWidth; private readonly TextParagraphProperties _paragraphProperties; @@ -570,7 +567,7 @@ namespace Avalonia.Media.TextFormatting { var characterIndex = firstTextSourceIndex + textLength; - var result = new List(TextRuns.Count); + var result = new List(_textRuns.Length); var lastDirection = FlowDirection.LeftToRight; var currentDirection = lastDirection; @@ -583,9 +580,9 @@ namespace Avalonia.Media.TextFormatting TextRunBounds lastRunBounds = default; - for (var index = 0; index < TextRuns.Count; index++) + for (var index = 0; index < _textRuns.Length; index++) { - if (TextRuns[index] is not DrawableTextRun currentRun) + if (_textRuns[index] is not DrawableTextRun currentRun) { continue; } @@ -671,12 +668,12 @@ namespace Avalonia.Media.TextFormatting for (int i = rightToLeftIndex - 1; i >= index; i--) { - if (TextRuns[i] is not ShapedTextRun) + if (_textRuns[i] is not ShapedTextRun shapedRun) { continue; } - currentShapedRun = (ShapedTextRun)TextRuns[i]; + currentShapedRun = shapedRun; currentRunBounds = GetRightToLeftTextRunBounds(currentShapedRun, startX, firstTextSourceIndex, characterIndex, currentPosition, remainingLength); @@ -786,7 +783,7 @@ namespace Avalonia.Media.TextFormatting { var characterIndex = firstTextSourceIndex + textLength; - var result = new List(TextRuns.Count); + var result = new List(_textRuns.Length); var lastDirection = FlowDirection.LeftToRight; var currentDirection = lastDirection; @@ -797,9 +794,9 @@ namespace Avalonia.Media.TextFormatting double currentWidth = 0; var currentRect = default(Rect); - for (var index = TextRuns.Count - 1; index >= 0; index--) + for (var index = _textRuns.Length - 1; index >= 0; index--) { - if (TextRuns[index] is not DrawableTextRun currentRun) + if (_textRuns[index] is not DrawableTextRun currentRun) { continue; } @@ -992,14 +989,11 @@ namespace Avalonia.Media.TextFormatting } } - public TextLineImpl FinalizeLine() + public void FinalizeLine() { _textLineMetrics = CreateLineMetrics(); - var bidiReorderer = s_bidiReorderer.Value!; - bidiReorderer.BidiReorder(_textRuns, _resolvedFlowDirection); - - return this; + BidiReorderer.Instance.BidiReorder(_textRuns, _resolvedFlowDirection); } /// diff --git a/src/Avalonia.Base/Utilities/ArrayBuilder.cs b/src/Avalonia.Base/Utilities/ArrayBuilder.cs index 3a22fc7b9c..bbbcc39ecc 100644 --- a/src/Avalonia.Base/Utilities/ArrayBuilder.cs +++ b/src/Avalonia.Base/Utilities/ArrayBuilder.cs @@ -12,7 +12,6 @@ namespace Avalonia.Utilities /// /// The type of item contained in the array. internal struct ArrayBuilder - where T : struct { private const int DefaultCapacity = 4; private const int MaxCoreClrArrayLength = 0x7FeFFFFF; @@ -48,6 +47,12 @@ namespace Avalonia.Utilities } } + /// + /// Gets the current capacity of the array. + /// + public int Capacity + => _data?.Length ?? 0; + /// /// Returns a reference to specified element of the array. /// @@ -131,8 +136,28 @@ namespace Avalonia.Utilities /// public void Clear() { - // No need to actually clear since we're not allowing reference types. +#if NET6_0_OR_GREATER + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + ClearArray(); + } + else + { + _size = 0; + } +#else + ClearArray(); +#endif + } + + private void ClearArray() + { + var size = _size; _size = 0; + if (size > 0) + { + Array.Clear(_data!, 0, size); + } } private void EnsureCapacity(int min) @@ -190,5 +215,12 @@ namespace Avalonia.Utilities /// The . [MethodImpl(MethodImplOptions.AggressiveInlining)] public ArraySlice AsSlice(int start, int length) => new ArraySlice(_data!, start, length); + + /// + /// Returns the current state of the array as a span. + /// + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Span AsSpan() => _data.AsSpan(0, _size); } } diff --git a/src/Avalonia.Base/Utilities/ArraySlice.cs b/src/Avalonia.Base/Utilities/ArraySlice.cs index 3cffef72c5..17e75f2f95 100644 --- a/src/Avalonia.Base/Utilities/ArraySlice.cs +++ b/src/Avalonia.Base/Utilities/ArraySlice.cs @@ -17,7 +17,6 @@ namespace Avalonia.Utilities /// /// The type of item contained in the slice. internal readonly struct ArraySlice : IReadOnlyList - where T : struct { /// /// Gets an empty diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 3218139251..b5f4777192 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -24,7 +24,10 @@ namespace Avalonia.UnitTests var glyphIndex = typeface.GetGlyph(codepoint); - shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster, 10); + for (var j = 0; j < count; ++j) + { + shapedBuffer[i + j] = new GlyphInfo(glyphIndex, glyphCluster, 10); + } i += count; } From fd0720fc5661932a9f4e9246ebcbd56fa0a513bf Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 00:11:28 +0100 Subject: [PATCH 12/23] Updated text layout benchmark with trimming/wrapping --- .../Avalonia.Benchmarks/Text/HugeTextLayout.cs | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index bf0253e9ab..8b23855fde 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -9,6 +9,8 @@ using BenchmarkDotNet.Attributes; namespace Avalonia.Benchmarks.Text; [MemoryDiagnoser] +[MinIterationTime(150)] +[MaxWarmupCount(15)] public class HugeTextLayout : IDisposable { private readonly IDisposable _app; @@ -44,7 +46,13 @@ Admitting that the possibility of achieving the results of the constructive crit Everyone understands what it takes to the draft analysis and prior decisions and early design solutions. In any case, we can systematically change the mechanism of the sources and influences of the continuing financing doctrine. This could exceedingly be a result of a task analysis the hardware maintenance. The real reason of the strategic planning seemingly the influence on eventual productivity. Everyone understands what it takes to the well-known practice. Therefore, the concept of the productivity boost can be treated as the only solution the driving factor. It may reveal how the matters of peculiar interest slowly the goals and objectives or the diverse sources of information the positive influence of any major outcomes complete failure of the supposed theory. In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; - + + [Params(false, true)] + public bool UseWrapping { get; set; } + + [Params(false, true)] + public bool UseTrimming { get; set; } + [Benchmark] public TextLayout BuildTextLayout() => MakeLayout(Text); @@ -91,9 +99,12 @@ In respect that the structure of the sufficient amount poses problems and challe } } - private static TextLayout MakeLayout(string str) + private TextLayout MakeLayout(string str) { - var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120); + var wrapping = UseWrapping ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; + var trimming = UseTrimming ? TextTrimming.CharacterEllipsis : TextTrimming.None; + var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120, + textTrimming: trimming, textWrapping: wrapping); layout.Dispose(); return layout; } From 7fcfc82be0d6375e61b7b92c7307afa04633ecc3 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 00:39:50 +0100 Subject: [PATCH 13/23] Fixed TextShaperImpl when the text is backed by an array --- src/Skia/Avalonia.Skia/TextShaperImpl.cs | 2 ++ src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs | 2 ++ tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index e0f95bac60..def2482af3 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.Skia if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index fffa5ce490..ac441108e3 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.Direct2D1.Media if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } diff --git a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs index 566cb0f1ac..baf5ffb07c 100644 --- a/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/HarfBuzzTextShaperImpl.cs @@ -161,6 +161,8 @@ namespace Avalonia.UnitTests if (MemoryMarshal.TryGetArray(memory, out var segment)) { + start = segment.Offset; + length = segment.Count; return segment.Array.AsMemory(); } From d714f37fce8b01b38f2ad61fc2bd2d7fb981b88a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 19 Jan 2023 11:12:09 +0600 Subject: [PATCH 14/23] Implemented interop with externally managed GPU memory --- Avalonia.Desktop.slnf | 1 + Avalonia.sln | 7 + build/Base.props | 1 + build/SharpDX.props | 15 +- .../ControlCatalog/Pages/OpenGlPage.xaml.cs | 21 +- samples/GpuInterop/App.axaml | 8 + samples/GpuInterop/App.axaml.cs | 22 + .../GpuInterop/D3DDemo/D3D11DemoControl.cs | 147 ++++ samples/GpuInterop/D3DDemo/D3D11Swapchain.cs | 119 +++ samples/GpuInterop/D3DDemo/D3DContent.cs | 110 +++ samples/GpuInterop/D3DDemo/MiniCube.fx | 47 + samples/GpuInterop/DrawingSurfaceDemoBase.cs | 141 +++ samples/GpuInterop/GpuDemo.axaml | 32 + samples/GpuInterop/GpuDemo.axaml.cs | 116 +++ samples/GpuInterop/GpuInterop.csproj | 50 ++ samples/GpuInterop/MainWindow.axaml | 13 + samples/GpuInterop/MainWindow.axaml.cs | 21 + samples/GpuInterop/Program.cs | 15 + .../VulkanDemo/Assets/Shaders/Makefile | 12 + .../VulkanDemo/Assets/Shaders/frag.glsl | 42 + .../VulkanDemo/Assets/Shaders/frag.spirv | Bin 0 -> 4276 bytes .../VulkanDemo/Assets/Shaders/vert.glsl | 36 + .../VulkanDemo/Assets/Shaders/vert.spirv | Bin 0 -> 3276 bytes samples/GpuInterop/VulkanDemo/ByteString.cs | 47 + .../GpuInterop/VulkanDemo/D3DMemoryHelper.cs | 54 ++ .../VulkanDemo/VulkanBufferHelper.cs | 80 ++ .../VulkanDemo/VulkanCommandBufferPool.cs | 224 +++++ .../GpuInterop/VulkanDemo/VulkanContent.cs | 829 ++++++++++++++++++ .../GpuInterop/VulkanDemo/VulkanContext.cs | 335 +++++++ .../VulkanDemo/VulkanDemoControl.cs | 101 +++ .../GpuInterop/VulkanDemo/VulkanExtensions.cs | 12 + samples/GpuInterop/VulkanDemo/VulkanImage.cs | 276 ++++++ .../VulkanDemo/VulkanMemoryHelper.cs | 59 ++ .../VulkanDemo/VulkanSemaphorePair.cs | 58 ++ .../GpuInterop/VulkanDemo/VulkanSwapchain.cs | 154 ++++ ...nalObjectsRenderInterfaceContextFeature.cs | 53 ++ .../Platform/IOptionalFeatureProvider.cs | 11 +- .../Platform/IPlatformRenderInterface.cs | 5 + .../PlatformGraphicsExternalMemory.cs | 64 ++ .../Composition/CompositingRenderer.cs | 6 +- .../Composition/CompositionDrawingSurface.cs | 60 ++ .../Composition/CompositionExternalMemory.cs | 142 +++ .../Composition/CompositionInterop.cs | 150 ++++ .../Composition/CompositionSurface.cs | 10 + .../Composition/Compositor.Factories.cs | 6 +- .../Rendering/Composition/Compositor.cs | 88 +- .../Server/ServerCompositionDrawingSurface.cs | 74 ++ .../Server/ServerCompositionSurface.cs | 9 +- .../Server/ServerCompositionSurfaceVisual.cs | 48 + .../Composition/Server/ServerCompositor.cs | 19 +- .../PlatformRenderInterfaceContextManager.cs | 2 + src/Avalonia.Base/Rendering/SwapchainBase.cs | 89 ++ src/Avalonia.Base/composition-schema.xml | 7 +- .../HeadlessPlatformRenderInterface.cs | 1 + src/Avalonia.OpenGL/Avalonia.OpenGL.csproj | 4 +- .../Controls/CompositionOpenGlSwapchain.cs | 163 ++++ .../Controls/OpenGlControlBase.cs | 302 +++---- .../Controls/OpenGlControlResources.cs | 170 ++++ src/Avalonia.OpenGL/Egl/EglConsts.cs | 12 +- src/Avalonia.OpenGL/Egl/EglContext.cs | 16 +- src/Avalonia.OpenGL/Egl/EglDisplay.cs | 2 +- src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs | 2 +- src/Avalonia.OpenGL/Egl/EglInterface.cs | 3 + .../ExternalObjectsOpenGlExtensionFeature.cs | 282 ++++++ src/Avalonia.OpenGL/GlConsts.cs | 14 +- .../IGlContextExternalObjectsFeature.cs | 50 ++ ...ureSharingRenderInterfaceContextFeature.cs | 13 +- .../IPlatformGraphicsOpenGlContextFactory.cs | 8 + .../Imaging/IOpenGlBitmapImpl.cs | 19 - src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs | 40 - src/Avalonia.OpenGL/OpenGlException.cs | 3 + src/Avalonia.X11/Glx/GlxContext.cs | 13 +- src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 7 - .../OpenGl/GlSkiaExternalObjectsFeature.cs | 191 ++++ .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 42 +- .../GlSkiaSharedTextureForComposition.cs | 44 + .../Gpu/OpenGl/OpenGlBitmapImpl.cs | 210 ----- src/Skia/Avalonia.Skia/ImmutableBitmap.cs | 7 + .../Avalonia.Skia/PlatformRenderInterface.cs | 5 - src/Skia/Avalonia.Skia/SkiaBackendContext.cs | 4 +- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 1 + .../Avalonia.Win32/DirectX/DirectXEnums.cs | 38 + .../Avalonia.Win32/DirectX/DirectXStructs.cs | 4 +- .../Avalonia.Win32/DirectX/directx.idl | 33 +- .../Angle/AngleExternalD3D11Texture2D.cs | 107 +++ .../Angle/AngleExternalObjectsFeature.cs | 103 +++ .../OpenGl/Angle/AngleWin32EglDisplay.cs | 132 ++- .../Angle/AngleWin32PlatformGraphics.cs | 18 +- src/Windows/Avalonia.Win32/Win32GlManager.cs | 3 + .../VisualTree/MockRenderInterface.cs | 2 + .../NullRenderingPlatform.cs | 2 + .../MockPlatformRenderInterface.cs | 2 + 92 files changed, 5541 insertions(+), 579 deletions(-) create mode 100644 samples/GpuInterop/App.axaml create mode 100644 samples/GpuInterop/App.axaml.cs create mode 100644 samples/GpuInterop/D3DDemo/D3D11DemoControl.cs create mode 100644 samples/GpuInterop/D3DDemo/D3D11Swapchain.cs create mode 100644 samples/GpuInterop/D3DDemo/D3DContent.cs create mode 100644 samples/GpuInterop/D3DDemo/MiniCube.fx create mode 100644 samples/GpuInterop/DrawingSurfaceDemoBase.cs create mode 100644 samples/GpuInterop/GpuDemo.axaml create mode 100644 samples/GpuInterop/GpuDemo.axaml.cs create mode 100644 samples/GpuInterop/GpuInterop.csproj create mode 100644 samples/GpuInterop/MainWindow.axaml create mode 100644 samples/GpuInterop/MainWindow.axaml.cs create mode 100644 samples/GpuInterop/Program.cs create mode 100644 samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile create mode 100644 samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl create mode 100644 samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv create mode 100644 samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl create mode 100644 samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv create mode 100644 samples/GpuInterop/VulkanDemo/ByteString.cs create mode 100644 samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanContent.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanContext.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanExtensions.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanImage.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs create mode 100644 samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs create mode 100644 src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs create mode 100644 src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs create mode 100644 src/Avalonia.Base/Rendering/SwapchainBase.cs create mode 100644 src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs create mode 100644 src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs create mode 100644 src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs create mode 100644 src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs create mode 100644 src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs delete mode 100644 src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs delete mode 100644 src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs create mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs create mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs delete mode 100644 src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs create mode 100644 src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 3fa8e969c8..2f034bd083 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -5,6 +5,7 @@ "packages\\Avalonia\\Avalonia.csproj", "samples\\ControlCatalog.NetCore\\ControlCatalog.NetCore.csproj", "samples\\ControlCatalog\\ControlCatalog.csproj", + "samples\\GpuInterop\\GpuInterop.csproj", "samples\\IntegrationTestApp\\IntegrationTestApp.csproj", "samples\\MiniMvvm\\MiniMvvm.csproj", "samples\\SampleControls\\ControlSamples.csproj", diff --git a/Avalonia.sln b/Avalonia.sln index fc42a5d63b..ce9a37a3ce 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -231,6 +231,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.Browser.Blaz EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReactiveUIDemo", "samples\ReactiveUIDemo\ReactiveUIDemo.csproj", "{75C47156-C5D8-44BC-A5A7-E8657C2248D6}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GpuInterop", "samples\GpuInterop\GpuInterop.csproj", "{C810060E-3809-4B74-A125-F11533AF9C1B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -542,6 +544,10 @@ Global {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Debug|Any CPU.Build.0 = Debug|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.ActiveCfg = Release|Any CPU {75C47156-C5D8-44BC-A5A7-E8657C2248D6}.Release|Any CPU.Build.0 = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C810060E-3809-4B74-A125-F11533AF9C1B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -606,6 +612,7 @@ Global {15B93A4C-1B46-43F6-B534-7B25B6E99932} = {9B9E3891-2366-4253-A952-D08BCEB71098} {90B08091-9BBD-4362-B712-E9F2CC62B218} = {9B9E3891-2366-4253-A952-D08BCEB71098} {75C47156-C5D8-44BC-A5A7-E8657C2248D6} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {C810060E-3809-4B74-A125-F11533AF9C1B} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/build/Base.props b/build/Base.props index 9ec1c3c2d3..433ce8e950 100644 --- a/build/Base.props +++ b/build/Base.props @@ -1,6 +1,7 @@  + diff --git a/build/SharpDX.props b/build/SharpDX.props index 69aa817a01..ff521977fd 100644 --- a/build/SharpDX.props +++ b/build/SharpDX.props @@ -1,9 +1,14 @@  + + 4.0.1 + - - - - - + + + + + + + diff --git a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs index ded02330d5..c77d65ddf1 100644 --- a/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/OpenGlPage.xaml.cs @@ -78,12 +78,7 @@ namespace ControlCatalog.Pages get => _info; private set => SetAndRaise(InfoProperty, ref _info, value); } - - static OpenGlPageControl() - { - AffectsRender(YawProperty, PitchProperty, RollProperty, DiscoProperty); - } - + private int _vertexShader; private int _fragmentShader; private int _shaderProgram; @@ -254,7 +249,7 @@ namespace ControlCatalog.Pages Console.WriteLine(err); } - protected unsafe override void OnOpenGlInit(GlInterface GL, int fb) + protected override unsafe void OnOpenGlInit(GlInterface GL) { CheckError(GL); @@ -309,7 +304,7 @@ namespace ControlCatalog.Pages } - protected override void OnOpenGlDeinit(GlInterface GL, int fb) + protected override void OnOpenGlDeinit(GlInterface GL) { // Unbind everything GL.BindBuffer(GL_ARRAY_BUFFER, 0); @@ -366,7 +361,15 @@ namespace ControlCatalog.Pages CheckError(GL); if (_disco > 0.01) - Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background); + RequestNextFrameRendering(); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == YawProperty || change.Property == RollProperty || change.Property == PitchProperty || + change.Property == DiscoProperty) + RequestNextFrameRendering(); + base.OnPropertyChanged(change); } } } diff --git a/samples/GpuInterop/App.axaml b/samples/GpuInterop/App.axaml new file mode 100644 index 0000000000..6b18dbe520 --- /dev/null +++ b/samples/GpuInterop/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/samples/GpuInterop/App.axaml.cs b/samples/GpuInterop/App.axaml.cs new file mode 100644 index 0000000000..85973d59bb --- /dev/null +++ b/samples/GpuInterop/App.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace GpuInterop +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + desktopLifetime.MainWindow = new MainWindow(); + } + } + } +} diff --git a/samples/GpuInterop/D3DDemo/D3D11DemoControl.cs b/samples/GpuInterop/D3DDemo/D3D11DemoControl.cs new file mode 100644 index 0000000000..c7ee2bb9a4 --- /dev/null +++ b/samples/GpuInterop/D3DDemo/D3D11DemoControl.cs @@ -0,0 +1,147 @@ +using System; +using System.Diagnostics; +using System.Linq; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using SharpDX; +using SharpDX.Direct2D1; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.Mathematics.Interop; +using Buffer = SharpDX.Direct3D11.Buffer; +using DeviceContext = SharpDX.Direct2D1.DeviceContext; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +using Matrix = SharpDX.Matrix; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiResource = SharpDX.DXGI.Resource; +using FeatureLevel = SharpDX.Direct3D.FeatureLevel; +using Vector3 = SharpDX.Vector3; + +namespace GpuInterop.D3DDemo; + +public class D3D11DemoControl : DrawingSurfaceDemoBase +{ + private D3DDevice _device; + private D3D11Swapchain _swapchain; + private SharpDX.Direct3D11.DeviceContext _context; + private Matrix _view; + private PixelSize _lastSize; + private Texture2D _depthBuffer; + private DepthStencilView _depthView; + private Matrix _proj; + private Buffer _constantBuffer; + private Stopwatch _st = Stopwatch.StartNew(); + + protected override (bool success, string info) InitializeGraphicsResources(Compositor compositor, + CompositionDrawingSurface surface, ICompositionGpuInterop interop) + { + if (interop?.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .D3D11TextureGlobalSharedHandle) != true) + return (false, "DXGI shared handle import is not supported by the current graphics backend"); + + var factory = new DxgiFactory1(); + using var adapter = factory.GetAdapter1(0); + _device = new D3DDevice(adapter, DeviceCreationFlags.None, new[] + { + FeatureLevel.Level_12_1, + FeatureLevel.Level_12_0, + FeatureLevel.Level_11_1, + FeatureLevel.Level_11_0, + FeatureLevel.Level_10_0, + FeatureLevel.Level_9_3, + FeatureLevel.Level_9_2, + FeatureLevel.Level_9_1, + }); + _swapchain = new D3D11Swapchain(_device, interop, surface); + _context = _device.ImmediateContext; + _constantBuffer = D3DContent.CreateMesh(_device); + _view = Matrix.LookAtLH(new Vector3(0, 0, -5), new Vector3(0, 0, 0), Vector3.UnitY); + return (true, $"D3D11 ({_device.FeatureLevel}) {adapter.Description1.Description}"); + } + + protected override void FreeGraphicsResources() + { + _swapchain.DisposeAsync(); + _swapchain = null!; + Utilities.Dispose(ref _depthView); + Utilities.Dispose(ref _depthBuffer); + Utilities.Dispose(ref _constantBuffer); + Utilities.Dispose(ref _context); + Utilities.Dispose(ref _device); + } + + protected override bool SupportsDisco => true; + + protected override void RenderFrame(PixelSize pixelSize) + { + if (pixelSize == default) + return; + if (pixelSize != _lastSize) + Resize(pixelSize); + using (_swapchain.BeginDraw(pixelSize, out var renderView)) + { + + _device.ImmediateContext.OutputMerger.SetTargets(_depthView, renderView); + var viewProj = Matrix.Multiply(_view, _proj); + var context = _device.ImmediateContext; + + var now = _st.Elapsed.TotalSeconds * 5; + var scaleX = (float)(1f + Disco * (Math.Sin(now) + 1) / 6); + var scaleY = (float)(1f + Disco * (Math.Cos(now) + 1) / 8); + var colorOff =(float) (Math.Sin(now) + 1) / 2 * Disco; + + + // Clear views + context.ClearDepthStencilView(_depthView, DepthStencilClearFlags.Depth, 1.0f, 0); + context.ClearRenderTargetView(renderView, + new RawColor4(1 - colorOff, colorOff, (float)0.5 + colorOff / 2, 1)); + + + var ypr = Matrix4x4.CreateFromYawPitchRoll(Yaw, Pitch, Roll); + // Update WorldViewProj Matrix + var worldViewProj = Matrix.RotationX((float)Yaw) * Matrix.RotationY((float)Pitch) + * Matrix.RotationZ((float)Roll) + * Matrix.Scaling(new Vector3(scaleX, scaleY, 1)) + * viewProj; + worldViewProj.Transpose(); + context.UpdateSubresource(ref worldViewProj, _constantBuffer); + + // Draw the cube + context.Draw(36, 0); + + + _context.Flush(); + } + } + + private void Resize(PixelSize size) + { + Utilities.Dispose(ref _depthBuffer); + _depthBuffer = new Texture2D(_device, + new Texture2DDescription() + { + Format = Format.D32_Float_S8X24_UInt, + ArraySize = 1, + MipLevels = 1, + Width = (int)size.Width, + Height = (int)size.Height, + SampleDescription = new SampleDescription(1, 0), + Usage = ResourceUsage.Default, + BindFlags = BindFlags.DepthStencil, + CpuAccessFlags = CpuAccessFlags.None, + OptionFlags = ResourceOptionFlags.None + }); + + Utilities.Dispose(ref _depthView); + _depthView = new DepthStencilView(_device, _depthBuffer); + + // Setup targets and viewport for rendering + _device.ImmediateContext.Rasterizer.SetViewport(new Viewport(0, 0, (int)size.Width, (int)size.Height, 0.0f, 1.0f)); + + // Setup new projection matrix with correct aspect ratio + _proj = Matrix.PerspectiveFovLH((float)Math.PI / 4.0f, (float)(size.Width / size.Height), 0.1f, 100.0f); + } +} diff --git a/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs b/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs new file mode 100644 index 0000000000..30a4c19d35 --- /dev/null +++ b/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using SharpDX.Direct2D1; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.Mathematics.Interop; +using Buffer = SharpDX.Direct3D11.Buffer; +using DeviceContext = SharpDX.Direct2D1.DeviceContext; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +using Matrix = SharpDX.Matrix; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiResource = SharpDX.DXGI.Resource; +using FeatureLevel = SharpDX.Direct3D.FeatureLevel; + +namespace GpuInterop.D3DDemo; + +class D3D11Swapchain : SwapchainBase +{ + private readonly D3DDevice _device; + + public D3D11Swapchain(D3DDevice device, ICompositionGpuInterop interop, CompositionDrawingSurface target) + : base(interop, target) + { + _device = device; + } + + protected override D3D11SwapchainImage CreateImage(PixelSize size) => new(_device, size, Interop, Target); + + public IDisposable BeginDraw(PixelSize size, out RenderTargetView view) + { + var rv = BeginDrawCore(size, out var image); + view = image.RenderTargetView; + return rv; + } +} + +public class D3D11SwapchainImage : ISwapchainImage +{ + public PixelSize Size { get; } + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _target; + private readonly Texture2D _texture; + private readonly KeyedMutex _mutex; + private readonly IntPtr _handle; + private PlatformGraphicsExternalImageProperties _properties; + private ICompositionImportedGpuImage? _imported; + public Task? LastPresent { get; private set; } + public RenderTargetView RenderTargetView { get; } + + public D3D11SwapchainImage(D3DDevice device, PixelSize size, + ICompositionGpuInterop interop, + CompositionDrawingSurface target) + { + Size = size; + _interop = interop; + _target = target; + _texture = new Texture2D(device, + new Texture2DDescription + { + Format = Format.R8G8B8A8_UNorm, + Width = size.Width, + Height = size.Height, + ArraySize = 1, + MipLevels = 1, + SampleDescription = new SampleDescription { Count = 1, Quality = 0 }, + CpuAccessFlags = default, + OptionFlags = ResourceOptionFlags.SharedKeyedmutex, + BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource + }); + _mutex = _texture.QueryInterface(); + using (var res = _texture.QueryInterface()) + _handle = res.SharedHandle; + _properties = new PlatformGraphicsExternalImageProperties + { + Width = size.Width, Height = size.Height, Format = PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm + }; + + RenderTargetView = new RenderTargetView(device, _texture); + } + + public void BeginDraw() + { + _mutex.Acquire(0, int.MaxValue); + } + + public void Present() + { + _mutex.Release(1); + _imported ??= _interop.ImportImage( + new PlatformHandle(_handle, KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle), + _properties); + LastPresent = _target.UpdateWithKeyedMutexAsync(_imported, 1, 0); + } + + + public async ValueTask DisposeAsync() + { + if (LastPresent != null) + try + { + await LastPresent; + } + catch + { + // Ignore + } + + RenderTargetView.Dispose(); + _mutex.Dispose(); + _texture.Dispose(); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/D3DDemo/D3DContent.cs b/samples/GpuInterop/D3DDemo/D3DContent.cs new file mode 100644 index 0000000000..f670a9a8c9 --- /dev/null +++ b/samples/GpuInterop/D3DDemo/D3DContent.cs @@ -0,0 +1,110 @@ +using SharpDX; +using SharpDX.D3DCompiler; +using SharpDX.Direct3D; + +using System; +using System.Linq; +using System.Threading.Tasks; +using SharpDX.Direct2D1; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using SharpDX.Mathematics.Interop; +using Buffer = SharpDX.Direct3D11.Buffer; +using DeviceContext = SharpDX.Direct2D1.DeviceContext; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +using Matrix = SharpDX.Matrix; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiResource = SharpDX.DXGI.Resource; +using FeatureLevel = SharpDX.Direct3D.FeatureLevel; +using InputElement = SharpDX.Direct3D11.InputElement; + + +namespace GpuInterop.D3DDemo; + +public class D3DContent +{ + + public static Buffer CreateMesh(D3DDevice device) + { + // Compile Vertex and Pixel shaders + var vertexShaderByteCode = ShaderBytecode.CompileFromFile("D3DDemo\\MiniCube.fx", "VS", "vs_4_0"); + var vertexShader = new VertexShader(device, vertexShaderByteCode); + + var pixelShaderByteCode = ShaderBytecode.CompileFromFile("D3DDemo\\MiniCube.fx", "PS", "ps_4_0"); + var pixelShader = new PixelShader(device, pixelShaderByteCode); + + var signature = ShaderSignature.GetInputSignature(vertexShaderByteCode); + + var inputElements = new[] + { + new InputElement("POSITION", 0, Format.R32G32B32A32_Float, 0, 0), + new InputElement("COLOR", 0, Format.R32G32B32A32_Float, 16, 0) + }; + + // Layout from VertexShader input signature + var layout = new InputLayout( + device, + signature, + inputElements); + + // Instantiate Vertex buffer from vertex data + using var vertices = Buffer.Create( + device, + BindFlags.VertexBuffer, + new[] + { + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), // Front + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), // BACK + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), // Top + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), // Bottom + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 1.0f, 0.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), // Left + new Vector4(-1.0f, -1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, -1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, 1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(-1.0f, 1.0f, -1.0f, 1.0f), new Vector4(1.0f, 0.0f, 1.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), // Right + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, -1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, -1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, -1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + new Vector4(1.0f, 1.0f, 1.0f, 1.0f), new Vector4(0.0f, 1.0f, 1.0f, 1.0f), + }); + + // Create Constant Buffer + var constantBuffer = new Buffer(device, Utilities.SizeOf(), ResourceUsage.Default, + BindFlags.ConstantBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0); + + var context = device.ImmediateContext; + + // Prepare All the stages + context.InputAssembler.InputLayout = layout; + context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList; + context.InputAssembler.SetVertexBuffers(0, + new VertexBufferBinding(vertices, Utilities.SizeOf() * 2, 0)); + context.VertexShader.SetConstantBuffer(0, constantBuffer); + context.VertexShader.Set(vertexShader); + context.PixelShader.Set(pixelShader); + return constantBuffer; + } +} diff --git a/samples/GpuInterop/D3DDemo/MiniCube.fx b/samples/GpuInterop/D3DDemo/MiniCube.fx new file mode 100644 index 0000000000..c6064ea56c --- /dev/null +++ b/samples/GpuInterop/D3DDemo/MiniCube.fx @@ -0,0 +1,47 @@ +// Copyright (c) 2010-2013 SharpDX - Alexandre Mutel +// +// 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. +struct VS_IN +{ + float4 pos : POSITION; + float4 col : COLOR; +}; + +struct PS_IN +{ + float4 pos : SV_POSITION; + float4 col : COLOR; +}; + +float4x4 worldViewProj; + +PS_IN VS( VS_IN input ) +{ + PS_IN output = (PS_IN)0; + + output.pos = mul(input.pos, worldViewProj); + output.col = input.col; + + return output; +} + +float4 PS( PS_IN input ) : SV_Target +{ + return input.col; +} diff --git a/samples/GpuInterop/DrawingSurfaceDemoBase.cs b/samples/GpuInterop/DrawingSurfaceDemoBase.cs new file mode 100644 index 0000000000..aad813ea82 --- /dev/null +++ b/samples/GpuInterop/DrawingSurfaceDemoBase.cs @@ -0,0 +1,141 @@ +using System; +using System.Numerics; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.LogicalTree; +using Avalonia.Rendering.Composition; +using Avalonia.VisualTree; + +namespace GpuInterop; + +public abstract class DrawingSurfaceDemoBase : Control, IGpuDemo +{ + private CompositionSurfaceVisual? _visual; + private Compositor? _compositor; + private Action _update; + private string _info; + private bool _updateQueued; + private bool _initialized; + + protected CompositionDrawingSurface Surface { get; private set; } + + public DrawingSurfaceDemoBase() + { + _update = UpdateFrame; + } + + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + Initialize(); + } + + protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e) + { + if (_initialized) + FreeGraphicsResources(); + _initialized = false; + base.OnDetachedFromLogicalTree(e); + } + + async void Initialize() + { + try + { + var selfVisual = ElementComposition.GetElementVisual(this)!; + _compositor = selfVisual.Compositor; + + Surface = _compositor.CreateDrawingSurface(); + _visual = _compositor.CreateSurfaceVisual(); + _visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + _visual.Surface = Surface; + ElementComposition.SetElementChildVisual(this, _visual); + var (res, info) = await DoInitialize(_compositor, Surface); + _info = info; + if (ParentControl != null) + ParentControl.Info = info; + _initialized = res; + QueueNextFrame(); + } + catch (Exception e) + { + if (ParentControl != null) + ParentControl.Info = e.ToString(); + } + } + + void UpdateFrame() + { + _updateQueued = false; + var root = this.GetVisualRoot(); + if (root == null) + return; + + _visual!.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + var size = PixelSize.FromSize(Bounds.Size, root.RenderScaling); + RenderFrame(size); + if (SupportsDisco && Disco > 0) + QueueNextFrame(); + } + + void QueueNextFrame() + { + if (_initialized && !_updateQueued && _compositor != null) + { + _updateQueued = true; + _compositor?.RequestCompositionUpdate(_update); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if(change.Property == BoundsProperty) + QueueNextFrame(); + base.OnPropertyChanged(change); + } + + async Task<(bool success, string info)> DoInitialize(Compositor compositor, + CompositionDrawingSurface compositionDrawingSurface) + { + var interop = await compositor.TryGetCompositionGpuInterop(); + if (interop == null) + return (false, "Compositor doesn't support interop for the current backend"); + return InitializeGraphicsResources(compositor, compositionDrawingSurface, interop); + } + + protected abstract (bool success, string info) InitializeGraphicsResources(Compositor compositor, + CompositionDrawingSurface compositionDrawingSurface, ICompositionGpuInterop gpuInterop); + + protected abstract void FreeGraphicsResources(); + + + protected abstract void RenderFrame(PixelSize pixelSize); + protected virtual bool SupportsDisco => false; + + public void Update(GpuDemo parent, float yaw, float pitch, float roll, float disco) + { + ParentControl = parent; + if (ParentControl != null) + { + ParentControl.Info = _info; + ParentControl.DiscoVisible = true; + } + + Yaw = yaw; + Pitch = pitch; + Roll = roll; + Disco = disco; + QueueNextFrame(); + } + + public GpuDemo? ParentControl { get; private set; } + + public float Disco { get; private set; } + + public float Roll { get; private set; } + + public float Pitch { get; private set; } + + public float Yaw { get; private set; } +} diff --git a/samples/GpuInterop/GpuDemo.axaml b/samples/GpuInterop/GpuDemo.axaml new file mode 100644 index 0000000000..6ca51abadb --- /dev/null +++ b/samples/GpuInterop/GpuDemo.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + Yaw + + Pitch + + Roll + + + + D + I + S + C + O + + + + + + + diff --git a/samples/GpuInterop/GpuDemo.axaml.cs b/samples/GpuInterop/GpuDemo.axaml.cs new file mode 100644 index 0000000000..f7acac09e1 --- /dev/null +++ b/samples/GpuInterop/GpuDemo.axaml.cs @@ -0,0 +1,116 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace GpuInterop; + +public class GpuDemo : UserControl +{ + public GpuDemo() + { + AvaloniaXamlLoader.Load(this); + } + + private float _yaw = 5; + + public static readonly DirectProperty YawProperty = + AvaloniaProperty.RegisterDirect("Yaw", o => o.Yaw, (o, v) => o.Yaw = v); + + public float Yaw + { + get => _yaw; + set => SetAndRaise(YawProperty, ref _yaw, value); + } + + private float _pitch = 5; + + public static readonly DirectProperty PitchProperty = + AvaloniaProperty.RegisterDirect("Pitch", o => o.Pitch, (o, v) => o.Pitch = v); + + public float Pitch + { + get => _pitch; + set => SetAndRaise(PitchProperty, ref _pitch, value); + } + + + private float _roll = 5; + + public static readonly DirectProperty RollProperty = + AvaloniaProperty.RegisterDirect("Roll", o => o.Roll, (o, v) => o.Roll = v); + + public float Roll + { + get => _roll; + set => SetAndRaise(RollProperty, ref _roll, value); + } + + + private float _disco; + + public static readonly DirectProperty DiscoProperty = + AvaloniaProperty.RegisterDirect("Disco", o => o.Disco, (o, v) => o.Disco = v); + + public float Disco + { + get => _disco; + set => SetAndRaise(DiscoProperty, ref _disco, value); + } + + private string _info = string.Empty; + + public static readonly DirectProperty InfoProperty = + AvaloniaProperty.RegisterDirect("Info", o => o.Info, (o, v) => o.Info = v); + + public string Info + { + get => _info; + set => SetAndRaise(InfoProperty, ref _info, value); + } + + private bool _discoVisible; + + public static readonly DirectProperty DiscoVisibleProperty = + AvaloniaProperty.RegisterDirect("DiscoVisible", o => o.DiscoVisible, + (o, v) => o._discoVisible = v); + + public bool DiscoVisible + { + get => _discoVisible; + set => SetAndRaise(DiscoVisibleProperty, ref _discoVisible, value); + } + + private IGpuDemo _demo; + + public static readonly DirectProperty DemoProperty = + AvaloniaProperty.RegisterDirect("Demo", o => o.Demo, + (o, v) => o._demo = v); + + public IGpuDemo Demo + { + get => _demo; + set => SetAndRaise(DemoProperty, ref _demo, value); + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == YawProperty + || change.Property == PitchProperty + || change.Property == RollProperty + || change.Property == DiscoProperty + || change.Property == DemoProperty + ) + { + if (change.Property == DemoProperty) + ((IGpuDemo)change.OldValue)?.Update(null, 0, 0, 0, 0); + _demo?.Update(this, Yaw, Pitch, Roll, Disco); + } + + base.OnPropertyChanged(change); + } +} + +public interface IGpuDemo +{ + void Update(GpuDemo parent, float yaw, float pitch, float roll, float disco); +} diff --git a/samples/GpuInterop/GpuInterop.csproj b/samples/GpuInterop/GpuInterop.csproj new file mode 100644 index 0000000000..c201d9bf85 --- /dev/null +++ b/samples/GpuInterop/GpuInterop.csproj @@ -0,0 +1,50 @@ + + + + Exe + net7.0 + true + enable + false + true + true + true + + + + + + + + + + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/GpuInterop/MainWindow.axaml b/samples/GpuInterop/MainWindow.axaml new file mode 100644 index 0000000000..afb025ec27 --- /dev/null +++ b/samples/GpuInterop/MainWindow.axaml @@ -0,0 +1,13 @@ + + + + + + + + diff --git a/samples/GpuInterop/MainWindow.axaml.cs b/samples/GpuInterop/MainWindow.axaml.cs new file mode 100644 index 0000000000..8fc8926783 --- /dev/null +++ b/samples/GpuInterop/MainWindow.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace GpuInterop +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.AttachDevTools(); + this.Renderer.DrawFps = true; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/GpuInterop/Program.cs b/samples/GpuInterop/Program.cs new file mode 100644 index 0000000000..86fd239b4c --- /dev/null +++ b/samples/GpuInterop/Program.cs @@ -0,0 +1,15 @@ +using Avalonia; + +namespace GpuInterop +{ + public class Program + { + static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + public static AppBuilder BuildAvaloniaApp() => + AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace(); + } +} diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile b/samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile new file mode 100644 index 0000000000..900e35a93e --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/Assets/Shaders/Makefile @@ -0,0 +1,12 @@ +#!/usr/bin/make -f + +all: vert.spirv frag.spirv +.PHONY: all + +vert.spirv: vert.glsl + glslc -fshader-stage=vert vert.glsl -o vert.spirv + +frag.spirv: frag.glsl + glslc -fshader-stage=frag frag.glsl -o frag.spirv + + diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl b/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl new file mode 100644 index 0000000000..31e9f00390 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.glsl @@ -0,0 +1,42 @@ +#version 450 + layout(location = 0) in vec3 FragPos; + layout(location = 1) in vec3 VecPos; + layout(location = 2) in vec3 Normal; + layout(push_constant) uniform constants{ + layout(offset = 0) float maxY; + layout(offset = 4) float minY; + layout(offset = 8) float time; + layout(offset = 12) float disco; + }; + layout(location = 0) out vec4 outFragColor; + + void main() + { + float y = (VecPos.y - minY) / (maxY - minY); + float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + time * 40.0 + y * 50.0); + float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - time * 20.0 - y * 30.0); + + vec3 discoColor = vec3( + 0.5 + abs(0.5 - y) * cos(time * 10.0), + 0.25 + (smoothstep(0.3, 0.8, y) * (0.5 - c / 4.0)), + 0.25 + abs((smoothstep(0.1, 0.4, y) * (0.5 - s / 4.0)))); + + vec3 objectColor = vec3((1.0 - y), 0.40 + y / 4.0, y * 0.75 + 0.25); + objectColor = objectColor * (1.0 - disco) + discoColor * disco; + + float ambientStrength = 0.3; + vec3 lightColor = vec3(1.0, 1.0, 1.0); + vec3 lightPos = vec3(maxY * 2.0, maxY * 2.0, maxY * 2.0); + vec3 ambient = ambientStrength * lightColor; + + + vec3 norm = normalize(Normal); + vec3 lightDir = normalize(lightPos - FragPos); + + float diff = max(dot(norm, lightDir), 0.0); + vec3 diffuse = diff * lightColor; + + vec3 result = (ambient + diffuse) * objectColor; + outFragColor = vec4(result, 1.0); + + } \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv b/samples/GpuInterop/VulkanDemo/Assets/Shaders/frag.spirv new file mode 100644 index 0000000000000000000000000000000000000000..dbac67c368fb0ce01b53b33504c8ae970f6e94f2 GIT binary patch literal 4276 zcmZ9OS9erp6owCwAiaoyqGAFT6hu@&6oG^m&8ud{s6D>zj*15Yxz89&JN?8wKn_R?=IioUzspu@bnv!WKc398J+x*l)v%GV3;Hs znG8*JQ}g=f**&F>*>mU3F=9+IBn>oXY%)BlK`UKt`GSs-$Z6ypaupdzynYq}4WW9H z{7G^Rt2Q;aHmzOT)V!j#eb1iOp3;HNTx(~(kZbM8cjwwm`Mo*ghE>Gn3+foHQD_{5@XaA*q z7oA;?b4NPzJ?%xb>rc?hC$2}gXLvr^HSJ>+I?Y*D>@0Sdxm|F@ZMzt~OxDs#A6#44 zwtTKoT3_nU6?T+%rum2AJM%ksW}zu}G`MtHYt|&EuIHzAKX#$m-GxeR&uR^JTW7h4 zGuUa}<$1a)$9R@2yW4hH)|%(BJM!DNhyPM$_x9vc|7Gm%Tu*Oj$^4<|Jt+2;ERju{ zBu&}bOeNNRUPYv5<2Svm1~~n4o^!-`4sxD#a6>X~6uODMx1MpX<(#e+wlit_>G!UN zJ%K3W>ZxH(@g&etbX_Vn>3#N2%S|SKc!rIy!ybXQZ~29p?Ol|ch;5H`h~Lc29u3xS z{+J9~e=4gui?~vZyAjSc^t+C>_fO7sZbH`~)^#22iJ5KwByjW|{wZ*N(bu%HIbh#~ z?VjkjpL>Od`s{4M49hKO%X{nr+Sy|ru)P*v4O~vi;35u8}EyJ7bZNh;M2UGrM==5P4&~_mdIditiy}ZqE24#F%pnVxD~D z-3m6(cM^HGf#oCSc5roVe_!OI_8nmJ)}4Ie&2P>GYjlIGm)ryKUhBU9stYv-utpz zbHLWq7kLkYoh$Ml2Fv-o5P5UK=IR?o$M!rAv1iX$zxz{k*?Wpr4cr_v)b(+D}sX_F+kG`L$ zkm?!NWPHrnf-SceiF)h6*7F?oN9@yJ*U}&NU_ID=^hM9lfbDq{|9UT;MXcu?+K6mI zjEVTo;Cdw9*mGdHElAA$JlJ*gML#cqt*tNmc@ew;(HHgGz?J=M!;XGh!CMhyBEB8$ zIYd7lV7bbEa@h8xFZ$UIwzj_5%N=0X@@(`+pF6?!slSC?8jj8*?x!>8_dcvb%SUgW zU}uSa=>qRYXPs9NYw3&HuY%pDsQntay7udE z@=@m?*gSb>v-Tmx`ubu9IdLD_yDL77K7z!0$H1;P1&LYS0L#Z-o&d`oMsY|5qUT%;`tVN6uNWeT4l9_#EOru*Ro|G4fIOGjP<^Z{0I! z`Ka?b*tNpG05+zdx!TpwTgV8Pgf8+}Kcf@?-_3uVp^D0{3zf@R&{0DbqS7HDF literal 0 HcmV?d00001 diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl b/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl new file mode 100644 index 0000000000..c700d78d25 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.glsl @@ -0,0 +1,36 @@ +#version 450 + layout(location = 0) in vec3 aPos; + layout(location = 1) in vec3 aNormal; + + layout(location = 0) out vec3 FragPos; + layout(location = 1) out vec3 VecPos; + layout(location = 2) out vec3 Normal; + + layout(push_constant) uniform constants{ + float maxY; + float minY; + float time; + float disco; + mat4 model; + }; + + layout(binding = 0) uniform UniformBufferObject { + mat4 projection; + } ubo; + + void main() + { + float discoScale = sin(time * 10.0) / 10.0; + float distortionX = 1.0 + disco * cos(time * 20.0) / 10.0; + + float scale = 1.0 + disco * discoScale; + + vec3 scaledPos = aPos; + scaledPos.x = scaledPos.x * distortionX; + + scaledPos *= scale; + gl_Position = ubo.projection * model * vec4(scaledPos, 1.0); + FragPos = vec3(model * vec4(aPos, 1.0)); + VecPos = aPos; + Normal = normalize(vec3(model * vec4(aNormal, 1.0))); + } \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv b/samples/GpuInterop/VulkanDemo/Assets/Shaders/vert.spirv new file mode 100644 index 0000000000000000000000000000000000000000..3bb2ba938342b011a5860a1d243c47065c78a7dc GIT binary patch literal 3276 zcmZ9MYj;ys5QYzJQUtk($VJ84QoMoI8z735UZAO3X|YhjTS$|%9!!&(q%QQ6esC>6 zxt9C_e~Z7$FRtbDoScC}cGq-f-kCk~&ffcEbYyC8lI%(LCkK;mGFV5F5s)N#IvLA! zWqECRy4P<^pTF>q8HbY5EYY09WVuSnb}emdPa`wPEu@CjkxgWjzY+95Y%I~SvRtjK ztW=h-SL-`F)n5PSR2;Np)ZALDPes;kZJdb8Va zKEMSX;M&GIz0@Y4d$W9J%J8(^UrT?<=NQDCjo3mf-MI$6TD#tSw9grh?Y3HvV$S9L zy2{j_Mc;0xo1O0V{O;yvv%9i!pZm)=8FTG)J7Q8|&uB6aw!6{6E@kgzv0K}6M19}P zdb6HQtnd4|LGc=gs_$E><-iii-~}eh6u2ueXBP8L0&`X|-$-DiId&k!N?ASklnXif z?t5kkwUzVV}U5ub#&b7&vO8beQH zQsNaQnTVaB-afRp^Pc4YezchWIGLV>@uHYBL5BBet~r-;-|tZD82%yj1R}R{xqld2 zUhemfqrY)`97TxEg?}>hf4v&}pT~E$h!4){?>T2V3vLbL)<1`KUUB`m&~nk2ufFd` zOuqi;P9QGp>%W_MkNuDE%|Au_GTMG(<5<3xn8%o6-z#W28Mm+gOpe7|SMgorWX_`F zjOVaDFERJ(&m&^iUdV0pqt+s}_h-CGeXQJ;LG{=7WwIRP=@IAv# zWDjzYFGF7Tc3Ie&$2Ml}42q>F>Cm4-bVb6jCo)Ba`4{XL&W@!qUZZL9{zcJxd(>}*ye~w zor~B}$3FVzMU4-zp{#DmLc*mAi^j&E@p5pxChIkuSZA+Q@c<~OyAnrHcxcBlG zb&ahe_BTh)e(PT#{?<5`bL*QIGu_4>o@pISJotZ!?fnL?JJ@2b!0uv;75RUaV@3X7 zgSp>)<3;}8;M?1rsQWFpxGS*lu*HhKsyR03HJ07^Juvs1Z`}EBq3 (byte*)h.Pointer; +} + +unsafe class ByteStringList : IDisposable +{ + private List _inner; + private byte** _ptr; + + public ByteStringList(IEnumerable items) + { + _inner = items.Select(x => new ByteString(x)).ToList(); + _ptr = (byte**)Marshal.AllocHGlobal(IntPtr.Size * _inner.Count + 1); + for (var c = 0; c < _inner.Count; c++) + _ptr[c] = (byte*)_inner[c].Pointer; + } + + public int Count => _inner.Count; + public uint UCount => (uint)_inner.Count; + + public void Dispose() + { + Marshal.FreeHGlobal(new IntPtr(_ptr)); + } + + public static implicit operator byte**(ByteStringList h) => h._ptr; +} diff --git a/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs b/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs new file mode 100644 index 0000000000..a0b7d32d3b --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/D3DMemoryHelper.cs @@ -0,0 +1,54 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia; +using SharpDX.Direct3D; +using SharpDX.Direct3D11; +using SharpDX.DXGI; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiFactory1 = SharpDX.DXGI.Factory1; +namespace GpuInterop.VulkanDemo; + +public class D3DMemoryHelper +{ + public static D3DDevice CreateDeviceByLuid(Span luid) + { + var factory = new DxgiFactory1(); + var longLuid = MemoryMarshal.Cast(luid)[0]; + for (var c = 0; c < factory.GetAdapterCount1(); c++) + { + using var adapter = factory.GetAdapter1(0); + if (adapter.Description1.Luid != longLuid) + continue; + + return new D3DDevice(adapter, DeviceCreationFlags.None, + new[] + { + FeatureLevel.Level_12_1, FeatureLevel.Level_12_0, FeatureLevel.Level_11_1, + FeatureLevel.Level_11_0, FeatureLevel.Level_10_0, FeatureLevel.Level_9_3, + FeatureLevel.Level_9_2, FeatureLevel.Level_9_1, + }); + } + + throw new ArgumentException("Device with the corresponding LUID not found"); + } + + public static Texture2D CreateMemoryHandle(D3DDevice device, PixelSize size, Silk.NET.Vulkan.Format format) + { + if (format != Silk.NET.Vulkan.Format.R8G8B8A8Unorm) + throw new ArgumentException("Not supported format"); + return new Texture2D(device, + new Texture2DDescription + { + Format = Format.R8G8B8A8_UNorm, + Width = size.Width, + Height = size.Height, + ArraySize = 1, + MipLevels = 1, + SampleDescription = new SampleDescription { Count = 1, Quality = 0 }, + CpuAccessFlags = default, + OptionFlags = ResourceOptionFlags.SharedKeyedmutex, + BindFlags = BindFlags.RenderTarget | BindFlags.ShaderResource + }); + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs b/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs new file mode 100644 index 0000000000..949a951a36 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanBufferHelper.cs @@ -0,0 +1,80 @@ +using System; +using System.Runtime.CompilerServices; +using Silk.NET.Vulkan; +using SilkNetDemo; +using Buffer = Silk.NET.Vulkan.Buffer; +using SystemBuffer = System.Buffer; + +namespace GpuInterop.VulkanDemo; + +static class VulkanBufferHelper +{ + public unsafe static void AllocateBuffer(VulkanContext vk, + BufferUsageFlags bufferUsageFlags, + out Buffer buffer, out DeviceMemory memory, + Span initialData) where T:unmanaged + { + var api = vk.Api; + var device = vk.Device; + + var size = Unsafe.SizeOf() * initialData.Length; + var bufferInfo = new BufferCreateInfo() + { + SType = StructureType.BufferCreateInfo, + Size = (ulong)size, + Usage = bufferUsageFlags, + SharingMode = SharingMode.Exclusive + }; + api.CreateBuffer(device, bufferInfo, null, out buffer).ThrowOnError(); + + api.GetBufferMemoryRequirements(device, buffer, out var memoryRequirements); + + var physicalDevice = vk.PhysicalDevice; + + var memoryAllocateInfo = new MemoryAllocateInfo + { + SType = StructureType.MemoryAllocateInfo, + AllocationSize = memoryRequirements.Size, + MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api, + physicalDevice, + memoryRequirements.MemoryTypeBits, + MemoryPropertyFlags.MemoryPropertyHostCoherentBit | + MemoryPropertyFlags.MemoryPropertyHostVisibleBit) + }; + + api.AllocateMemory(device, memoryAllocateInfo, null, out memory).ThrowOnError(); + api.BindBufferMemory(device, buffer, memory, 0); + UpdateBufferMemory(vk, memory, initialData); + } + + public static unsafe void UpdateBufferMemory(VulkanContext vk, DeviceMemory memory, + Span data) where T : unmanaged + { + var api = vk.Api; + var device = vk.Device; + + var size = data.Length * Unsafe.SizeOf(); + void* pointer = null; + api.MapMemory(device, memory, 0, (ulong)size, 0, ref pointer); + + data.CopyTo(new Span(pointer, size)); + + api.UnmapMemory(device, memory); + + } + + private static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, + MemoryPropertyFlags flags) + { + api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties); + + for (var i = 0; i < properties.MemoryTypeCount; i++) + { + var type = properties.MemoryTypes[i]; + + if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i; + } + + return -1; + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs b/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs new file mode 100644 index 0000000000..65a05c5226 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanCommandBufferPool.cs @@ -0,0 +1,224 @@ +using System; +using System.Collections.Generic; +using Avalonia.Input; +using Silk.NET.Vulkan; +using SilkNetDemo; + +namespace Avalonia.Vulkan +{ + public class VulkanCommandBufferPool : IDisposable + { + private readonly Vk _api; + private readonly Device _device; + private readonly Queue _queue; + private readonly CommandPool _commandPool; + + private readonly List _usedCommandBuffers = new(); + private object _lock = new object(); + + public unsafe VulkanCommandBufferPool(Vk api, Device device, Queue queue, uint queueFamilyIndex) + { + _api = api; + _device = device; + _queue = queue; + + var commandPoolCreateInfo = new CommandPoolCreateInfo + { + SType = StructureType.CommandPoolCreateInfo, + Flags = CommandPoolCreateFlags.CommandPoolCreateResetCommandBufferBit, + QueueFamilyIndex = queueFamilyIndex + }; + + _api.CreateCommandPool(_device, commandPoolCreateInfo, null, out _commandPool) + .ThrowOnError(); + } + + public unsafe void Dispose() + { + lock (_lock) + { + FreeUsedCommandBuffers(); + _api.DestroyCommandPool(_device, _commandPool, null); + } + } + + private CommandBuffer AllocateCommandBuffer() + { + var commandBufferAllocateInfo = new CommandBufferAllocateInfo + { + SType = StructureType.CommandBufferAllocateInfo, + CommandPool = _commandPool, + CommandBufferCount = 1, + Level = CommandBufferLevel.Primary + }; + + lock (_lock) + { + _api.AllocateCommandBuffers(_device, commandBufferAllocateInfo, out var commandBuffer); + + return commandBuffer; + } + } + + public VulkanCommandBuffer CreateCommandBuffer() + { + return new(_api, _device, _queue, this); + } + + public void FreeUsedCommandBuffers() + { + lock (_lock) + { + foreach (var usedCommandBuffer in _usedCommandBuffers) usedCommandBuffer.Dispose(); + + _usedCommandBuffers.Clear(); + } + } + + private void DisposeCommandBuffer(VulkanCommandBuffer commandBuffer) + { + lock (_lock) + { + _usedCommandBuffers.Add(commandBuffer); + } + } + + public class VulkanCommandBuffer : IDisposable + { + private readonly VulkanCommandBufferPool _commandBufferPool; + private readonly Vk _api; + private readonly Device _device; + private readonly Queue _queue; + private readonly Fence _fence; + private bool _hasEnded; + private bool _hasStarted; + + public IntPtr Handle => InternalHandle.Handle; + + internal CommandBuffer InternalHandle { get; } + + internal unsafe VulkanCommandBuffer(Vk api, Device device, Queue queue, VulkanCommandBufferPool commandBufferPool) + { + _api = api; + _device = device; + _queue = queue; + _commandBufferPool = commandBufferPool; + + InternalHandle = _commandBufferPool.AllocateCommandBuffer(); + + var fenceCreateInfo = new FenceCreateInfo() + { + SType = StructureType.FenceCreateInfo, + Flags = FenceCreateFlags.FenceCreateSignaledBit + }; + + api.CreateFence(device, fenceCreateInfo, null, out _fence); + } + + public unsafe void Dispose() + { + _api.WaitForFences(_device, 1, _fence, true, ulong.MaxValue); + lock (_commandBufferPool._lock) + { + _api.FreeCommandBuffers(_device, _commandBufferPool._commandPool, 1, InternalHandle); + } + _api.DestroyFence(_device, _fence, null); + } + + public void BeginRecording() + { + if (!_hasStarted) + { + _hasStarted = true; + + var beginInfo = new CommandBufferBeginInfo + { + SType = StructureType.CommandBufferBeginInfo, + Flags = CommandBufferUsageFlags.CommandBufferUsageOneTimeSubmitBit + }; + + _api.BeginCommandBuffer(InternalHandle, beginInfo); + } + } + + public void EndRecording() + { + if (_hasStarted && !_hasEnded) + { + _hasEnded = true; + + _api.EndCommandBuffer(InternalHandle); + } + } + + public void Submit() + { + Submit(null, null, null, _fence); + } + + public class KeyedMutexSubmitInfo + { + public ulong? AcquireKey { get; set; } + public ulong? ReleaseKey { get; set; } + public DeviceMemory DeviceMemory { get; set; } + } + + public unsafe void Submit( + ReadOnlySpan waitSemaphores, + ReadOnlySpan waitDstStageMask = default, + ReadOnlySpan signalSemaphores = default, + Fence? fence = null, + KeyedMutexSubmitInfo keyedMutex = null) + { + EndRecording(); + + if (!fence.HasValue) + fence = _fence; + + + ulong acquireKey = keyedMutex?.AcquireKey ?? 0, releaseKey = keyedMutex?.ReleaseKey ?? 0; + DeviceMemory devMem = keyedMutex?.DeviceMemory ?? default; + uint timeout = uint.MaxValue; + Win32KeyedMutexAcquireReleaseInfoKHR mutex = default; + if (keyedMutex != null) + mutex = new Win32KeyedMutexAcquireReleaseInfoKHR + { + SType = StructureType.Win32KeyedMutexAcquireReleaseInfoKhr, + AcquireCount = keyedMutex.AcquireKey.HasValue ? 1u : 0u, + ReleaseCount = keyedMutex.ReleaseKey.HasValue ? 1u : 0u, + PAcquireKeys = &acquireKey, + PReleaseKeys = &releaseKey, + PAcquireSyncs = &devMem, + PReleaseSyncs = &devMem, + PAcquireTimeouts = &timeout + }; + + fixed (Semaphore* pWaitSemaphores = waitSemaphores, pSignalSemaphores = signalSemaphores) + { + fixed (PipelineStageFlags* pWaitDstStageMask = waitDstStageMask) + { + var commandBuffer = InternalHandle; + var submitInfo = new SubmitInfo + { + PNext = keyedMutex != null ? &mutex : null, + SType = StructureType.SubmitInfo, + WaitSemaphoreCount = waitSemaphores != null ? (uint)waitSemaphores.Length : 0, + PWaitSemaphores = pWaitSemaphores, + PWaitDstStageMask = pWaitDstStageMask, + CommandBufferCount = 1, + PCommandBuffers = &commandBuffer, + SignalSemaphoreCount = signalSemaphores != null ? (uint)signalSemaphores.Length : 0, + PSignalSemaphores = pSignalSemaphores, + }; + + _api.ResetFences(_device, 1, fence.Value); + + _api.QueueSubmit(_queue, 1, submitInfo, fence.Value); + } + } + + _commandBufferPool.DisposeCommandBuffer(this); + } + } + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanContent.cs b/samples/GpuInterop/VulkanDemo/VulkanContent.cs new file mode 100644 index 0000000000..b16343190a --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanContent.cs @@ -0,0 +1,829 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Threading; +using Silk.NET.Vulkan; +using SilkNetDemo; +using Buffer = System.Buffer; +using Image = Silk.NET.Vulkan.Image; + +namespace GpuInterop.VulkanDemo; + +unsafe class VulkanContent : IDisposable +{ + private readonly VulkanContext _context; + private ShaderModule _vertShader; + private ShaderModule _fragShader; + private PipelineLayout _pipelineLayout; + private RenderPass _renderPass; + private Pipeline _pipeline; + private DescriptorSetLayout _descriptorSetLayout; + private Silk.NET.Vulkan.Buffer _vertexBuffer; + private DeviceMemory _vertexBufferMemory; + private Silk.NET.Vulkan.Buffer _indexBuffer; + private DeviceMemory _indexBufferMemory; + private Silk.NET.Vulkan.Buffer _uniformBuffer; + private DeviceMemory _uniformBufferMemory; + private Framebuffer _framebuffer; + + private Image _depthImage; + private DeviceMemory _depthImageMemory; + private ImageView _depthImageView; + + public VulkanContent(VulkanContext context) + { + _context = context; + var name = typeof(VulkanContent).Assembly.GetManifestResourceNames().First(x => x.Contains("teapot.bin")); + using (var sr = new BinaryReader(typeof(VulkanContent).Assembly.GetManifestResourceStream(name))) + { + var buf = new byte[sr.ReadInt32()]; + sr.Read(buf, 0, buf.Length); + var points = new float[buf.Length / 4]; + Buffer.BlockCopy(buf, 0, points, 0, buf.Length); + buf = new byte[sr.ReadInt32()]; + sr.Read(buf, 0, buf.Length); + _indices = new ushort[buf.Length / 2]; + Buffer.BlockCopy(buf, 0, _indices, 0, buf.Length); + _points = new Vertex[points.Length / 3]; + for (var primitive = 0; primitive < points.Length / 3; primitive++) + { + var srci = primitive * 3; + _points[primitive] = new Vertex + { + Position = new Vector3(points[srci], points[srci + 1], points[srci + 2]) + }; + } + + for (int i = 0; i < _indices.Length; i += 3) + { + Vector3 a = _points[_indices[i]].Position; + Vector3 b = _points[_indices[i + 1]].Position; + Vector3 c = _points[_indices[i + 2]].Position; + var normal = Vector3.Normalize(Vector3.Cross(c - b, a - b)); + + _points[_indices[i]].Normal += normal; + _points[_indices[i + 1]].Normal += normal; + _points[_indices[i + 2]].Normal += normal; + } + + for (int i = 0; i < _points.Length; i++) + { + _points[i].Normal = Vector3.Normalize(_points[i].Normal); + _maxY = Math.Max(_maxY, _points[i].Position.Y); + _minY = Math.Min(_minY, _points[i].Position.Y); + } + } + + var api = _context.Api; + var device = _context.Device; + var vertShaderData = GetShader(false); + var fragShaderData = GetShader(true); + + fixed (byte* ptr = vertShaderData) + { + var shaderCreateInfo = new ShaderModuleCreateInfo() + { + SType = StructureType.ShaderModuleCreateInfo, + CodeSize = (nuint)vertShaderData.Length, + PCode = (uint*)ptr, + }; + + api.CreateShaderModule(device, shaderCreateInfo, null, out _vertShader); + } + + fixed (byte* ptr = fragShaderData) + { + var shaderCreateInfo = new ShaderModuleCreateInfo() + { + SType = StructureType.ShaderModuleCreateInfo, + CodeSize = (nuint)fragShaderData.Length, + PCode = (uint*)ptr, + }; + + api.CreateShaderModule(device, shaderCreateInfo, null, out _fragShader); + } + + CreateBuffers(); + } + + private byte[] GetShader(bool fragment) + { + var name = typeof(VulkanContent).Assembly.GetManifestResourceNames() + .First(x => x.Contains((fragment ? "frag" : "vert") + ".spirv")); + using (var sr = typeof(VulkanContent).Assembly.GetManifestResourceStream(name)) + { + using (var mem = new MemoryStream()) + { + sr.CopyTo(mem); + return mem.ToArray(); + } + } + } + + private PixelSize? _previousImageSize = PixelSize.Empty; + + + public void Render(VulkanImage image, + double yaw, double pitch, double roll, double disco) + { + + var api = _context.Api; + + if (image.Size != _previousImageSize) + CreateTemporalObjects(image.Size); + + _previousImageSize = image.Size; + + + var model = Matrix4x4.CreateFromYawPitchRoll((float)yaw, (float)pitch, (float)roll); + var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0)); + var projection = + Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)((float)image.Size.Width / image.Size.Height), + 0.01f, 1000); + + var vertexConstant = new VertextPushConstant() + { + Disco = (float)disco, + MinY = _minY, + MaxY = _maxY, + Model = model, + Time = (float)St.Elapsed.TotalSeconds + }; + + var commandBuffer = _context.Pool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + + _colorAttachment.TransitionLayout(commandBuffer.InternalHandle, + ImageLayout.Undefined, AccessFlags.None, + ImageLayout.ColorAttachmentOptimal, AccessFlags.ColorAttachmentWriteBit); + + var commandBufferHandle = new CommandBuffer(commandBuffer.Handle); + + api.CmdSetViewport(commandBufferHandle, 0, 1, + new Viewport() + { + Width = (float)image.Size.Width, + Height = (float)image.Size.Height, + MaxDepth = 1, + MinDepth = 0, + X = 0, + Y = 0 + }); + + var scissor = new Rect2D + { + Extent = new Extent2D((uint?)image.Size.Width, (uint?)image.Size.Height) + }; + + api.CmdSetScissor(commandBufferHandle, 0, 1, &scissor); + + var clearColor = new ClearValue(new ClearColorValue(1, 0, 0, 0.1f), new ClearDepthStencilValue(1, 0)); + + var clearValues = new[] { clearColor, clearColor }; + + + fixed (ClearValue* clearValue = clearValues) + { + var beginInfo = new RenderPassBeginInfo() + { + SType = StructureType.RenderPassBeginInfo, + RenderPass = _renderPass, + Framebuffer = _framebuffer, + RenderArea = new Rect2D(new Offset2D(0, 0), new Extent2D((uint?)image.Size.Width, (uint?)image.Size.Height)), + ClearValueCount = 2, + PClearValues = clearValue + }; + + api.CmdBeginRenderPass(commandBufferHandle, beginInfo, SubpassContents.Inline); + } + + api.CmdBindPipeline(commandBufferHandle, PipelineBindPoint.Graphics, _pipeline); + + var dset = _descriptorSet; + api.CmdBindDescriptorSets(commandBufferHandle, PipelineBindPoint.Graphics, + _pipelineLayout,0,1, &dset, null); + + api.CmdPushConstants(commandBufferHandle, _pipelineLayout, ShaderStageFlags.ShaderStageVertexBit | ShaderStageFlags.FragmentBit, 0, + (uint)Marshal.SizeOf(), &vertexConstant); + api.CmdBindVertexBuffers(commandBufferHandle, 0, 1, _vertexBuffer, 0); + api.CmdBindIndexBuffer(commandBufferHandle, _indexBuffer, 0, IndexType.Uint16); + + api.CmdDrawIndexed(commandBufferHandle, (uint)_indices.Length, 1, 0, 0, 0); + + + api.CmdEndRenderPass(commandBufferHandle); + + _colorAttachment.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferSrcOptimal, AccessFlags.TransferReadBit); + image.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferDstOptimal, AccessFlags.TransferWriteBit); + + + var srcBlitRegion = new ImageBlit + { + SrcOffsets = new ImageBlit.SrcOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D(image.Size.Width, image.Size.Height, 1), + }, + DstOffsets = new ImageBlit.DstOffsetsBuffer + { + Element0 = new Offset3D(0, 0, 0), + Element1 = new Offset3D(image.Size.Width, image.Size.Height, 1), + }, + SrcSubresource = + new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ImageAspectColorBit, + BaseArrayLayer = 0, + LayerCount = 1, + MipLevel = 0 + }, + DstSubresource = new ImageSubresourceLayers + { + AspectMask = ImageAspectFlags.ImageAspectColorBit, + BaseArrayLayer = 0, + LayerCount = 1, + MipLevel = 0 + } + }; + + api.CmdBlitImage(commandBuffer.InternalHandle, _colorAttachment.InternalHandle.Value, + ImageLayout.TransferSrcOptimal, + image.InternalHandle.Value, ImageLayout.TransferDstOptimal, 1, srcBlitRegion, Filter.Linear); + + commandBuffer.Submit(); + } + + public unsafe void Dispose() + { + if (_isInit) + { + var api = _context.Api; + var device = _context.Device; + + DestroyTemporalObjects(); + + api.DestroyShaderModule(device, _vertShader, null); + api.DestroyShaderModule(device, _fragShader, null); + + api.DestroyBuffer(device, _vertexBuffer, null); + api.FreeMemory(device, _vertexBufferMemory, null); + + api.DestroyBuffer(device, _indexBuffer, null); + api.FreeMemory(device, _indexBufferMemory, null); + + + } + + _isInit = false; + } + + public unsafe void DestroyTemporalObjects() + { + if (_isInit) + { + if (_renderPass.Handle != 0) + { + var api = _context.Api; + var device = _context.Device; + api.FreeDescriptorSets(_context.Device, _context.DescriptorPool, new[] { _descriptorSet }); + + api.DestroyImageView(device, _depthImageView, null); + api.DestroyImage(device, _depthImage, null); + api.FreeMemory(device, _depthImageMemory, null); + + api.DestroyFramebuffer(device, _framebuffer, null); + api.DestroyPipeline(device, _pipeline, null); + api.DestroyPipelineLayout(device, _pipelineLayout, null); + api.DestroyRenderPass(device, _renderPass, null); + api.DestroyDescriptorSetLayout(device, _descriptorSetLayout, null); + + api.DestroyBuffer(device, _uniformBuffer, null); + api.FreeMemory(device, _uniformBufferMemory, null); + _colorAttachment?.Dispose(); + + _colorAttachment = null; + _depthImage = default; + _depthImageView = default; + _depthImageView = default; + _framebuffer = default; + _pipeline = default; + _renderPass = default; + _pipelineLayout = default; + _descriptorSetLayout = default; + _uniformBuffer = default; + _uniformBufferMemory = default; + } + } + } + + private unsafe void CreateDepthAttachment(PixelSize size) + { + var imageCreateInfo = new ImageCreateInfo + { + SType = StructureType.ImageCreateInfo, + ImageType = ImageType.ImageType2D, + Format = Format.D32Sfloat, + Extent = + new Extent3D((uint?)size.Width, + (uint?)size.Height, 1), + MipLevels = 1, + ArrayLayers = 1, + Samples = SampleCountFlags.SampleCount1Bit, + Tiling = ImageTiling.Optimal, + Usage = ImageUsageFlags.ImageUsageDepthStencilAttachmentBit, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + Flags = ImageCreateFlags.ImageCreateMutableFormatBit + }; + + var api = _context.Api; + var device = _context.Device; + api + .CreateImage(device, imageCreateInfo, null, out _depthImage).ThrowOnError(); + + api.GetImageMemoryRequirements(device, _depthImage, + out var memoryRequirements); + + var memoryAllocateInfo = new MemoryAllocateInfo + { + SType = StructureType.MemoryAllocateInfo, + AllocationSize = memoryRequirements.Size, + MemoryTypeIndex = (uint)FindSuitableMemoryTypeIndex(api, + _context.PhysicalDevice, + memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit) + }; + + api.AllocateMemory(device, memoryAllocateInfo, null, + out _depthImageMemory).ThrowOnError(); + + api.BindImageMemory(device, _depthImage, _depthImageMemory, 0); + + var componentMapping = new ComponentMapping( + ComponentSwizzle.R, + ComponentSwizzle.G, + ComponentSwizzle.B, + ComponentSwizzle.A); + + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectDepthBit, + 0, 1, 0, 1); + + var imageViewCreateInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = _depthImage, + ViewType = ImageViewType.ImageViewType2D, + Format = Format.D32Sfloat, + Components = componentMapping, + SubresourceRange = subresourceRange + }; + + api + .CreateImageView(device, imageViewCreateInfo, null, out _depthImageView) + .ThrowOnError(); + } + + private unsafe void CreateTemporalObjects(PixelSize size) + { + DestroyTemporalObjects(); + + var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0)); + var projection = + Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)((float)size.Width / size.Height), + 0.01f, 1000); + + _colorAttachment = new VulkanImage(_context, (uint)Format.R8G8B8A8Unorm, size, false); + CreateDepthAttachment(size); + + var api = _context.Api; + var device = _context.Device; + + // create renderpasses + var colorAttachment = new AttachmentDescription() + { + Format = Format.R8G8B8A8Unorm, + Samples = SampleCountFlags.SampleCount1Bit, + LoadOp = AttachmentLoadOp.Clear, + StoreOp = AttachmentStoreOp.Store, + InitialLayout = ImageLayout.Undefined, + FinalLayout = ImageLayout.ColorAttachmentOptimal, + StencilLoadOp = AttachmentLoadOp.DontCare, + StencilStoreOp = AttachmentStoreOp.DontCare + }; + + var depthAttachment = new AttachmentDescription() + { + Format = Format.D32Sfloat, + Samples = SampleCountFlags.SampleCount1Bit, + LoadOp = AttachmentLoadOp.Clear, + StoreOp = AttachmentStoreOp.DontCare, + InitialLayout = ImageLayout.Undefined, + FinalLayout = ImageLayout.DepthStencilAttachmentOptimal, + StencilLoadOp = AttachmentLoadOp.DontCare, + StencilStoreOp = AttachmentStoreOp.DontCare + }; + + var subpassDependency = new SubpassDependency() + { + SrcSubpass = Vk.SubpassExternal, + DstSubpass = 0, + SrcStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit, + SrcAccessMask = 0, + DstStageMask = PipelineStageFlags.PipelineStageColorAttachmentOutputBit, + DstAccessMask = AccessFlags.AccessColorAttachmentWriteBit + }; + + var colorAttachmentReference = new AttachmentReference() + { + Attachment = 0, Layout = ImageLayout.ColorAttachmentOptimal + }; + + var depthAttachmentReference = new AttachmentReference() + { + Attachment = 1, Layout = ImageLayout.DepthStencilAttachmentOptimal + }; + + var subpassDescription = new SubpassDescription() + { + PipelineBindPoint = PipelineBindPoint.Graphics, + ColorAttachmentCount = 1, + PColorAttachments = &colorAttachmentReference, + PDepthStencilAttachment = &depthAttachmentReference + }; + + var attachments = new[] { colorAttachment, depthAttachment }; + + fixed (AttachmentDescription* atPtr = attachments) + { + var renderPassCreateInfo = new RenderPassCreateInfo() + { + SType = StructureType.RenderPassCreateInfo, + AttachmentCount = (uint)attachments.Length, + PAttachments = atPtr, + SubpassCount = 1, + PSubpasses = &subpassDescription, + DependencyCount = 1, + PDependencies = &subpassDependency + }; + + api.CreateRenderPass(device, renderPassCreateInfo, null, out _renderPass).ThrowOnError(); + + + // create framebuffer + var frameBufferAttachments = new[] { new ImageView(_colorAttachment.ViewHandle), _depthImageView }; + + fixed (ImageView* frAtPtr = frameBufferAttachments) + { + var framebufferCreateInfo = new FramebufferCreateInfo() + { + SType = StructureType.FramebufferCreateInfo, + RenderPass = _renderPass, + AttachmentCount = (uint)frameBufferAttachments.Length, + PAttachments = frAtPtr, + Width = (uint)size.Width, + Height = (uint)size.Height, + Layers = 1 + }; + + api.CreateFramebuffer(device, framebufferCreateInfo, null, out _framebuffer).ThrowOnError(); + } + } + + // Create pipeline + var pname = Marshal.StringToHGlobalAnsi("main"); + var vertShaderStageInfo = new PipelineShaderStageCreateInfo() + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.ShaderStageVertexBit, + Module = _vertShader, + PName = (byte*)pname, + }; + var fragShaderStageInfo = new PipelineShaderStageCreateInfo() + { + SType = StructureType.PipelineShaderStageCreateInfo, + Stage = ShaderStageFlags.ShaderStageFragmentBit, + Module = _fragShader, + PName = (byte*)pname, + }; + + var stages = new[] { vertShaderStageInfo, fragShaderStageInfo }; + + var bindingDescription = Vertex.VertexInputBindingDescription; + var attributeDescription = Vertex.VertexInputAttributeDescription; + + fixed (VertexInputAttributeDescription* attrPtr = attributeDescription) + { + var vertextInputInfo = new PipelineVertexInputStateCreateInfo() + { + SType = StructureType.PipelineVertexInputStateCreateInfo, + VertexAttributeDescriptionCount = (uint)attributeDescription.Length, + VertexBindingDescriptionCount = 1, + PVertexAttributeDescriptions = attrPtr, + PVertexBindingDescriptions = &bindingDescription + }; + + var inputAssembly = new PipelineInputAssemblyStateCreateInfo() + { + SType = StructureType.PipelineInputAssemblyStateCreateInfo, + Topology = PrimitiveTopology.TriangleList, + PrimitiveRestartEnable = false + }; + + var viewport = new Viewport() + { + X = 0, + Y = 0, + Width = (float)size.Width, + Height = (float)size.Height, + MinDepth = 0, + MaxDepth = 1 + }; + + var scissor = new Rect2D() + { + Offset = new Offset2D(0, 0), Extent = new Extent2D((uint)viewport.Width, (uint)viewport.Height) + }; + + var pipelineViewPortCreateInfo = new PipelineViewportStateCreateInfo() + { + SType = StructureType.PipelineViewportStateCreateInfo, + ViewportCount = 1, + PViewports = &viewport, + ScissorCount = 1, + PScissors = &scissor + }; + + var rasterizerStateCreateInfo = new PipelineRasterizationStateCreateInfo() + { + SType = StructureType.PipelineRasterizationStateCreateInfo, + DepthClampEnable = false, + RasterizerDiscardEnable = false, + PolygonMode = PolygonMode.Fill, + LineWidth = 1, + CullMode = CullModeFlags.CullModeNone, + DepthBiasEnable = false + }; + + var multisampleStateCreateInfo = new PipelineMultisampleStateCreateInfo() + { + SType = StructureType.PipelineMultisampleStateCreateInfo, + SampleShadingEnable = false, + RasterizationSamples = SampleCountFlags.SampleCount1Bit + }; + + var depthStencilCreateInfo = new PipelineDepthStencilStateCreateInfo() + { + SType = StructureType.PipelineDepthStencilStateCreateInfo, + StencilTestEnable = false, + DepthCompareOp = CompareOp.Less, + DepthTestEnable = true, + DepthWriteEnable = true, + DepthBoundsTestEnable = false, + }; + + var colorBlendAttachmentState = new PipelineColorBlendAttachmentState() + { + ColorWriteMask = ColorComponentFlags.ColorComponentABit | + ColorComponentFlags.ColorComponentRBit | + ColorComponentFlags.ColorComponentGBit | + ColorComponentFlags.ColorComponentBBit, + BlendEnable = false + }; + + var colorBlendState = new PipelineColorBlendStateCreateInfo() + { + SType = StructureType.PipelineColorBlendStateCreateInfo, + LogicOpEnable = false, + AttachmentCount = 1, + PAttachments = &colorBlendAttachmentState + }; + + var dynamicStates = new DynamicState[] { DynamicState.Viewport, DynamicState.Scissor }; + + fixed (DynamicState* states = dynamicStates) + { + var dynamicStateCreateInfo = new PipelineDynamicStateCreateInfo() + { + SType = StructureType.PipelineDynamicStateCreateInfo, + DynamicStateCount = (uint)dynamicStates.Length, + PDynamicStates = states + }; + + var vertexPushConstantRange = new PushConstantRange() + { + Offset = 0, + Size = (uint)Marshal.SizeOf(), + StageFlags = ShaderStageFlags.ShaderStageVertexBit + }; + + var fragPushConstantRange = new PushConstantRange() + { + //Offset = vertexPushConstantRange.Size, + Size = (uint)Marshal.SizeOf(), + StageFlags = ShaderStageFlags.ShaderStageFragmentBit + }; + + var layoutBindingInfo = new DescriptorSetLayoutBinding + { + Binding = 0, + StageFlags = ShaderStageFlags.VertexBit, + DescriptorCount = 1, + DescriptorType = DescriptorType.UniformBuffer, + }; + + var layoutInfo = new DescriptorSetLayoutCreateInfo + { + SType = StructureType.DescriptorSetLayoutCreateInfo, + BindingCount = 1, + PBindings = &layoutBindingInfo + }; + + api.CreateDescriptorSetLayout(device, &layoutInfo, null, out _descriptorSetLayout).ThrowOnError(); + + var projView = view * projection; + VulkanBufferHelper.AllocateBuffer(_context, BufferUsageFlags.UniformBufferBit, + out _uniformBuffer, + out _uniformBufferMemory, new[] + { + new UniformBuffer + { + Projection = projView + } + }); + + var descriptorSetLayout = _descriptorSetLayout; + var descriptorCreateInfo = new DescriptorSetAllocateInfo + { + SType = StructureType.DescriptorSetAllocateInfo, + DescriptorPool = _context.DescriptorPool, + DescriptorSetCount = 1, + PSetLayouts = &descriptorSetLayout + }; + api.AllocateDescriptorSets(device, &descriptorCreateInfo, out _descriptorSet).ThrowOnError(); + + var descriptorBufferInfo = new DescriptorBufferInfo + { + Buffer = _uniformBuffer, + Range = (ulong)Unsafe.SizeOf(), + }; + var descriptorWrite = new WriteDescriptorSet + { + SType = StructureType.WriteDescriptorSet, + DstSet = _descriptorSet, + DescriptorType = DescriptorType.UniformBuffer, + DescriptorCount = 1, + PBufferInfo = &descriptorBufferInfo, + }; + api.UpdateDescriptorSets(device, 1, &descriptorWrite, 0, null); + + var constants = new[] { vertexPushConstantRange, fragPushConstantRange }; + + fixed (PushConstantRange* constant = constants) + { + var setLayout = _descriptorSetLayout; + var pipelineLayoutCreateInfo = new PipelineLayoutCreateInfo() + { + SType = StructureType.PipelineLayoutCreateInfo, + PushConstantRangeCount = (uint)constants.Length, + PPushConstantRanges = constant, + SetLayoutCount = 1, + PSetLayouts = &setLayout + }; + + api.CreatePipelineLayout(device, pipelineLayoutCreateInfo, null, out _pipelineLayout) + .ThrowOnError(); + } + + + fixed (PipelineShaderStageCreateInfo* stPtr = stages) + { + var pipelineCreateInfo = new GraphicsPipelineCreateInfo() + { + SType = StructureType.GraphicsPipelineCreateInfo, + StageCount = 2, + PStages = stPtr, + PVertexInputState = &vertextInputInfo, + PInputAssemblyState = &inputAssembly, + PViewportState = &pipelineViewPortCreateInfo, + PRasterizationState = &rasterizerStateCreateInfo, + PMultisampleState = &multisampleStateCreateInfo, + PDepthStencilState = &depthStencilCreateInfo, + PColorBlendState = &colorBlendState, + PDynamicState = &dynamicStateCreateInfo, + Layout = _pipelineLayout, + RenderPass = _renderPass, + Subpass = 0, + BasePipelineHandle = _pipeline.Handle != 0 ? _pipeline : new Pipeline(), + BasePipelineIndex = _pipeline.Handle != 0 ? 0 : -1 + }; + + api.CreateGraphicsPipelines(device, new PipelineCache(), 1, &pipelineCreateInfo, null, + out _pipeline).ThrowOnError(); + } + } + } + + Marshal.FreeHGlobal(pname); + _isInit = true; + } + + private unsafe void CreateBuffers() + { + VulkanBufferHelper.AllocateBuffer(_context, BufferUsageFlags.VertexBufferBit, out _vertexBuffer, + out _vertexBufferMemory, _points); + VulkanBufferHelper.AllocateBuffer(_context, BufferUsageFlags.IndexBufferBit, out _indexBuffer, + out _indexBufferMemory, _indices); + } + + private static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, + MemoryPropertyFlags flags) + { + api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties); + + for (var i = 0; i < properties.MemoryTypeCount; i++) + { + var type = properties.MemoryTypes[i]; + + if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i; + } + + return -1; + } + + + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct Vertex + { + public Vector3 Position; + public Vector3 Normal; + + public static unsafe VertexInputBindingDescription VertexInputBindingDescription + { + get + { + return new VertexInputBindingDescription() + { + Binding = 0, + Stride = (uint)Marshal.SizeOf(), + InputRate = VertexInputRate.Vertex + }; + } + } + + public static unsafe VertexInputAttributeDescription[] VertexInputAttributeDescription + { + get + { + return new VertexInputAttributeDescription[] + { + new VertexInputAttributeDescription + { + Binding = 0, + Location = 0, + Format = Format.R32G32B32Sfloat, + Offset = (uint)Marshal.OffsetOf("Position") + }, + new VertexInputAttributeDescription + { + Binding = 0, + Location = 1, + Format = Format.R32G32B32Sfloat, + Offset = (uint)Marshal.OffsetOf("Normal") + } + }; + } + } + } + + private readonly Vertex[] _points; + private readonly ushort[] _indices; + private readonly float _minY; + private readonly float _maxY; + + + static Stopwatch St = Stopwatch.StartNew(); + private bool _isInit; + private VulkanImage _colorAttachment; + private DescriptorSet _descriptorSet; + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct VertextPushConstant + { + public float MaxY; + public float MinY; + public float Time; + public float Disco; + public Matrix4x4 Model; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct UniformBuffer + { + public Matrix4x4 Projection; + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanContext.cs b/samples/GpuInterop/VulkanDemo/VulkanContext.cs new file mode 100644 index 0000000000..56041d6965 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanContext.cs @@ -0,0 +1,335 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Avalonia.Vulkan; +using Silk.NET.Core; +using Silk.NET.Core.Native; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.EXT; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; +using SkiaSharp; +using D3DDevice = SharpDX.Direct3D11.Device; +using DxgiDevice = SharpDX.DXGI.Device; + +namespace GpuInterop.VulkanDemo; + +public unsafe class VulkanContext : IDisposable +{ + public Vk Api { get; init; } + public Instance Instance { get; init; } + public PhysicalDevice PhysicalDevice { get; init; } + public Device Device { get; init; } + public Queue Queue { get; init; } + public uint QueueFamilyIndex { get; init; } + public VulkanCommandBufferPool Pool { get; init; } + public GRContext GrContext { get; init; } + public DescriptorPool DescriptorPool { get; init; } + public D3DDevice? D3DDevice { get; init; } + + public static (VulkanContext? result, string info) TryCreate(ICompositionGpuInterop gpuInterop) + { + using var appName = new ByteString("GpuInterop"); + using var engineName = new ByteString("Test"); + var applicationInfo = new ApplicationInfo + { + SType = StructureType.ApplicationInfo, + PApplicationName = appName, + ApiVersion = new Version32(1, 1, 0), + PEngineName = appName, + EngineVersion = new Version32(1, 0, 0), + ApplicationVersion = new Version32(1, 0, 0) + }; + + var enabledExtensions = new List() + { + "VK_KHR_get_physical_device_properties2", + "VK_KHR_external_memory_capabilities", + "VK_KHR_external_semaphore_capabilities" + }; + + var enabledLayers = new List(); + + Vk api = Vk.GetApi(); + enabledExtensions.Add("VK_EXT_debug_utils"); + if (IsLayerAvailable(api, "VK_LAYER_KHRONOS_validation")) + enabledLayers.Add("VK_LAYER_KHRONOS_validation"); + + + Instance vkInstance = default; + Silk.NET.Vulkan.PhysicalDevice physicalDevice = default; + Device device = default; + DescriptorPool descriptorPool = default; + VulkanCommandBufferPool? pool = null; + GRContext? grContext = null; + try + { + using var pRequiredExtensions = new ByteStringList(enabledExtensions); + using var pEnabledLayers = new ByteStringList(enabledLayers); + api.CreateInstance(new InstanceCreateInfo + { + SType = StructureType.InstanceCreateInfo, + PApplicationInfo = &applicationInfo, + PpEnabledExtensionNames = pRequiredExtensions, + EnabledExtensionCount = pRequiredExtensions.UCount, + PpEnabledLayerNames = pEnabledLayers, + EnabledLayerCount = pEnabledLayers.UCount + }, null, out vkInstance).ThrowOnError(); + + + if (api.TryGetInstanceExtension(vkInstance, out ExtDebugUtils debugUtils)) + { + var debugCreateInfo = new DebugUtilsMessengerCreateInfoEXT + { + SType = StructureType.DebugUtilsMessengerCreateInfoExt, + MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityVerboseBitExt | + DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityWarningBitExt | + DebugUtilsMessageSeverityFlagsEXT.DebugUtilsMessageSeverityErrorBitExt, + MessageType = DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeGeneralBitExt | + DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypeValidationBitExt | + DebugUtilsMessageTypeFlagsEXT.DebugUtilsMessageTypePerformanceBitExt, + PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(LogCallback), + }; + + debugUtils.CreateDebugUtilsMessenger(vkInstance, debugCreateInfo, null, out var messenger); + } + + var requireDeviceExtensions = new List + { + "VK_KHR_external_memory", + "VK_KHR_external_semaphore" + }; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + if (!gpuInterop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .D3D11TextureGlobalSharedHandle) + ) + return (null, "Image sharing is not supported by the current backend"); + requireDeviceExtensions.Add(KhrExternalMemoryWin32.ExtensionName); + requireDeviceExtensions.Add(KhrExternalSemaphoreWin32.ExtensionName); + requireDeviceExtensions.Add("VK_KHR_dedicated_allocation"); + requireDeviceExtensions.Add("VK_KHR_get_memory_requirements2"); + } + else + { + if (!gpuInterop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .VulkanOpaquePosixFileDescriptor) + || !gpuInterop.SupportedSemaphoreTypes.Contains(KnownPlatformGraphicsExternalSemaphoreHandleTypes + .VulkanOpaquePosixFileDescriptor) + ) + return (null, "Image sharing is not supported by the current backend"); + requireDeviceExtensions.Add(KhrExternalMemoryFd.ExtensionName); + requireDeviceExtensions.Add(KhrExternalSemaphoreFd.ExtensionName); + } + + uint count = 0; + api.EnumeratePhysicalDevices(vkInstance, ref count, null).ThrowOnError(); + var physicalDevices = stackalloc PhysicalDevice[(int)count]; + api.EnumeratePhysicalDevices(vkInstance, ref count, physicalDevices) + .ThrowOnError(); + + for (uint c = 0; c < count; c++) + { + if (requireDeviceExtensions.Any(ext => !api.IsDeviceExtensionPresent(physicalDevices[c], ext))) + continue; + + var physicalDeviceIDProperties = new PhysicalDeviceIDProperties() + { + SType = StructureType.PhysicalDeviceIDProperties + }; + var physicalDeviceProperties2 = new PhysicalDeviceProperties2() + { + SType = StructureType.PhysicalDeviceProperties2, + PNext = &physicalDeviceIDProperties + }; + api.GetPhysicalDeviceProperties2(physicalDevices[c], &physicalDeviceProperties2); + + if (gpuInterop.DeviceLuid != null && physicalDeviceIDProperties.DeviceLuidvalid) + { + if (!new Span(physicalDeviceIDProperties.DeviceLuid, 8) + .SequenceEqual(gpuInterop.DeviceLuid)) + continue; + } + else if (gpuInterop.DeviceUuid != null) + { + if (!new Span(physicalDeviceIDProperties.DeviceUuid, 16) + .SequenceEqual(gpuInterop?.DeviceUuid)) + continue; + } + + physicalDevice = physicalDevices[c]; + + var name = Marshal.PtrToStringAnsi(new IntPtr(physicalDeviceProperties2.Properties.DeviceName))!; + + + uint queueFamilyCount = 0; + api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, null); + var familyProperties = stackalloc QueueFamilyProperties[(int)queueFamilyCount]; + api.GetPhysicalDeviceQueueFamilyProperties(physicalDevice, ref queueFamilyCount, familyProperties); + for (uint queueFamilyIndex = 0; queueFamilyIndex < queueFamilyCount; queueFamilyIndex++) + { + var family = familyProperties[c]; + if (!family.QueueFlags.HasAllFlags(QueueFlags.GraphicsBit)) + continue; + + + var queuePriorities = stackalloc float[(int)family.QueueCount]; + + for (var i = 0; i < family.QueueCount; i++) + queuePriorities[i] = 1f; + + var features = new PhysicalDeviceFeatures(); + + var queueCreateInfo = new DeviceQueueCreateInfo + { + SType = StructureType.DeviceQueueCreateInfo, + QueueFamilyIndex = queueFamilyIndex, + QueueCount = family.QueueCount, + PQueuePriorities = queuePriorities + }; + + using var pEnabledDeviceExtensions = new ByteStringList(requireDeviceExtensions); + var deviceCreateInfo = new DeviceCreateInfo + { + SType = StructureType.DeviceCreateInfo, + QueueCreateInfoCount = 1, + PQueueCreateInfos = &queueCreateInfo, + PpEnabledExtensionNames = pEnabledDeviceExtensions, + EnabledExtensionCount = pEnabledDeviceExtensions.UCount, + PEnabledFeatures = &features + }; + + api.CreateDevice(physicalDevice, in deviceCreateInfo, null, out device) + .ThrowOnError(); + + api.GetDeviceQueue(device, queueFamilyIndex, 0, out var queue); + + var descriptorPoolSize = new DescriptorPoolSize + { + Type = DescriptorType.UniformBuffer, DescriptorCount = 16 + }; + var descriptorPoolInfo = new DescriptorPoolCreateInfo + { + SType = StructureType.DescriptorPoolCreateInfo, + PoolSizeCount = 1, + PPoolSizes = &descriptorPoolSize, + MaxSets = 16, + Flags = DescriptorPoolCreateFlags.FreeDescriptorSetBit + }; + + api.CreateDescriptorPool(device, &descriptorPoolInfo, null, out descriptorPool) + .ThrowOnError(); + + pool = new VulkanCommandBufferPool(api, device, queue, queueFamilyIndex); + grContext = GRContext.CreateVulkan(new GRVkBackendContext + { + VkInstance = vkInstance.Handle, + VkDevice = device.Handle, + VkQueue = queue.Handle, + GraphicsQueueIndex = queueFamilyIndex, + VkPhysicalDevice = physicalDevice.Handle, + GetProcedureAddress = (proc, _, _) => + { + var rv = api.GetDeviceProcAddr(device, proc); + if (rv != IntPtr.Zero) + return rv; + rv = api.GetInstanceProcAddr(vkInstance, proc); + if (rv != IntPtr.Zero) + return rv; + return api.GetInstanceProcAddr(default, proc); + } + }); + + + D3DDevice? d3dDevice = null; + if (physicalDeviceIDProperties.DeviceLuidvalid && + RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + d3dDevice = D3DMemoryHelper.CreateDeviceByLuid( + new Span(physicalDeviceIDProperties.DeviceLuid, 8)); + + var dxgiDevice = d3dDevice?.QueryInterface(); + return (new VulkanContext + { + Api = api, + Device = device, + Instance = vkInstance, + PhysicalDevice = physicalDevice, + Queue = queue, + QueueFamilyIndex = queueFamilyIndex, + Pool = pool, + DescriptorPool = descriptorPool, + GrContext = grContext, + D3DDevice = d3dDevice + }, name); + } + return (null, "No suitable device queue found"); + } + + return (null, "Suitable device not found"); + + } + catch (Exception e) + { + return (null, e.ToString()); + } + finally + { + if (grContext == null && api != null) + { + pool?.Dispose(); + if (descriptorPool.Handle != default) + api.DestroyDescriptorPool(device, descriptorPool, null); + if (device.Handle != default) + api.DestroyDevice(device, null); + } + } + } + + private static unsafe bool IsLayerAvailable(Vk api, string layerName) + { + uint layerPropertiesCount; + + api.EnumerateInstanceLayerProperties(&layerPropertiesCount, null).ThrowOnError(); + + var layerProperties = new LayerProperties[layerPropertiesCount]; + + fixed (LayerProperties* pLayerProperties = layerProperties) + { + api.EnumerateInstanceLayerProperties(&layerPropertiesCount, layerProperties).ThrowOnError(); + + for (var i = 0; i < layerPropertiesCount; i++) + { + var currentLayerName = Marshal.PtrToStringAnsi((IntPtr)pLayerProperties[i].LayerName); + + if (currentLayerName == layerName) return true; + } + } + + return false; + } + + private static unsafe uint LogCallback(DebugUtilsMessageSeverityFlagsEXT messageSeverity, DebugUtilsMessageTypeFlagsEXT messageTypes, DebugUtilsMessengerCallbackDataEXT* pCallbackData, void* pUserData) + { + if (messageSeverity != DebugUtilsMessageSeverityFlagsEXT.VerboseBitExt) + { + var message = Marshal.PtrToStringAnsi((nint)pCallbackData->PMessage); + Console.WriteLine(message); + } + + return Vk.False; + } + + public void Dispose() + { + D3DDevice?.Dispose(); + GrContext.Dispose(); + Pool.Dispose(); + Api.DestroyDescriptorPool(Device, DescriptorPool, null); + Api.DestroyDevice(Device, null); + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs b/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs new file mode 100644 index 0000000000..6a1cd641b3 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanDemoControl.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Silk.NET.Core; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; + +namespace GpuInterop.VulkanDemo; + +public class VulkanDemoControl : DrawingSurfaceDemoBase +{ + private Instance _vkInstance; + private Vk _api; + + class VulkanResources : IAsyncDisposable + { + public VulkanContext Context { get; } + public VulkanSwapchain Swapchain { get; } + public VulkanContent Content { get; } + + public VulkanResources(VulkanContext context, VulkanSwapchain swapchain, VulkanContent content) + { + Context = context; + Swapchain = swapchain; + Content = content; + } + public async ValueTask DisposeAsync() + { + Context.Pool.FreeUsedCommandBuffers(); + Content.Dispose(); + await Swapchain.DisposeAsync(); + Context.Dispose(); + } + } + + protected override bool SupportsDisco => true; + + private VulkanResources? _resources; + + protected override (bool success, string info) InitializeGraphicsResources(Compositor compositor, + CompositionDrawingSurface compositionDrawingSurface, ICompositionGpuInterop gpuInterop) + { + var (context, info) = VulkanContext.TryCreate(gpuInterop); + if (context == null) + return (false, info); + try + { + var content = new VulkanContent(context); + _resources = new VulkanResources(context, + new VulkanSwapchain(context, gpuInterop, compositionDrawingSurface), content); + return (true, info); + } + catch(Exception e) + { + return (false, e.ToString()); + } + } + + protected override void FreeGraphicsResources() + { + _resources?.DisposeAsync(); + _resources = null; + } + + protected override unsafe void RenderFrame(PixelSize pixelSize) + { + if (_resources == null) + return; + using (_resources.Swapchain.BeginDraw(pixelSize, out var image)) + { + /* + var commandBuffer = _resources.Context.Pool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + image.TransitionLayout(commandBuffer.InternalHandle, ImageLayout.TransferDstOptimal, AccessFlags.None); + + var range = new ImageSubresourceRange + { + AspectMask = ImageAspectFlags.ColorBit, + LayerCount = 1, + LevelCount = 1, + BaseArrayLayer = 0, + BaseMipLevel = 0 + }; + var color = new ClearColorValue + { + Float32_0 = 1, Float32_1 = 0, Float32_2 = 0, Float32_3 = 1 + }; + _resources.Context.Api.CmdClearColorImage(commandBuffer.InternalHandle, image.InternalHandle.Value, ImageLayout.TransferDstOptimal, + &color, 1, &range); + commandBuffer.Submit();*/ + _resources.Content.Render(image, Yaw, Pitch, Roll, Disco); + } + } +} diff --git a/samples/GpuInterop/VulkanDemo/VulkanExtensions.cs b/samples/GpuInterop/VulkanDemo/VulkanExtensions.cs new file mode 100644 index 0000000000..1a13ebd4c5 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanExtensions.cs @@ -0,0 +1,12 @@ +using System; +using Silk.NET.Vulkan; + +namespace SilkNetDemo; + +public static class VulkanExtensions +{ + public static void ThrowOnError(this Result result) + { + if (result != Result.Success) throw new Exception($"Unexpected API error \"{result}\"."); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanImage.cs b/samples/GpuInterop/VulkanDemo/VulkanImage.cs new file mode 100644 index 0000000000..e8854bfeb2 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanImage.cs @@ -0,0 +1,276 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Vulkan; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; +using SkiaSharp; + +namespace GpuInterop.VulkanDemo; + +public unsafe class VulkanImage : IDisposable + { + private readonly VulkanContext _vk; + private readonly Instance _instance; + private readonly Device _device; + private readonly PhysicalDevice _physicalDevice; + private readonly VulkanCommandBufferPool _commandBufferPool; + private ImageLayout _currentLayout; + private AccessFlags _currentAccessFlags; + private ImageUsageFlags _imageUsageFlags { get; } + private ImageView? _imageView { get; set; } + private DeviceMemory _imageMemory { get; set; } + private SharpDX.Direct3D11.Texture2D? _d3dTexture2D; + private IntPtr _win32ShareHandle; + + internal Image? InternalHandle { get; private set; } + internal Format Format { get; } + internal ImageAspectFlags AspectFlags { get; private set; } + + public ulong Handle => InternalHandle?.Handle ?? 0; + public ulong ViewHandle => _imageView?.Handle ?? 0; + public uint UsageFlags => (uint) _imageUsageFlags; + public ulong MemoryHandle => _imageMemory.Handle; + public DeviceMemory DeviceMemory => _imageMemory; + public uint MipLevels { get; private set; } + public Vk Api { get; } + public PixelSize Size { get; } + public ulong MemorySize { get; private set; } + public uint CurrentLayout => (uint) _currentLayout; + + public VulkanImage(VulkanContext vk, uint format, PixelSize size, + bool exportable, uint mipLevels = 0) + { + _vk = vk; + _instance = vk.Instance; + _device = vk.Device; + _physicalDevice = vk.PhysicalDevice; + _commandBufferPool = vk.Pool; + Format = (Format)format; + Api = vk.Api; + Size = size; + MipLevels = 1;//mipLevels; + _imageUsageFlags = + ImageUsageFlags.ImageUsageColorAttachmentBit | ImageUsageFlags.ImageUsageTransferDstBit | + ImageUsageFlags.ImageUsageTransferSrcBit | ImageUsageFlags.ImageUsageSampledBit; + + //MipLevels = MipLevels != 0 ? MipLevels : (uint)Math.Floor(Math.Log(Math.Max(Size.Width, Size.Height), 2)); + + var handleType = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + ExternalMemoryHandleTypeFlags.D3D11TextureKmtBit : + ExternalMemoryHandleTypeFlags.OpaqueFDBit; + var externalMemoryCreateInfo = new ExternalMemoryImageCreateInfo + { + SType = StructureType.ExternalMemoryImageCreateInfo, + HandleTypes = handleType + }; + + var imageCreateInfo = new ImageCreateInfo + { + PNext = exportable ? &externalMemoryCreateInfo : null, + SType = StructureType.ImageCreateInfo, + ImageType = ImageType.ImageType2D, + Format = Format, + Extent = + new Extent3D((uint?)Size.Width, + (uint?)Size.Height, 1), + MipLevels = MipLevels, + ArrayLayers = 1, + Samples = SampleCountFlags.SampleCount1Bit, + Tiling = Tiling, + Usage = _imageUsageFlags, + SharingMode = SharingMode.Exclusive, + InitialLayout = ImageLayout.Undefined, + Flags = ImageCreateFlags.ImageCreateMutableFormatBit + }; + + Api + .CreateImage(_device, imageCreateInfo, null, out var image).ThrowOnError(); + InternalHandle = image; + + Api.GetImageMemoryRequirements(_device, InternalHandle.Value, + out var memoryRequirements); + + + var fdExport = new ExportMemoryAllocateInfo + { + HandleTypes = handleType, SType = StructureType.ExportMemoryAllocateInfo + }; + var dedicatedAllocation = new MemoryDedicatedAllocateInfoKHR + { + SType = StructureType.MemoryDedicatedAllocateInfoKhr, + Image = image + }; + ImportMemoryWin32HandleInfoKHR handleImport = default; + if (exportable && RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _d3dTexture2D = D3DMemoryHelper.CreateMemoryHandle(vk.D3DDevice, size, Format); + using var dxgi = _d3dTexture2D.QueryInterface(); + _win32ShareHandle = dxgi.SharedHandle; + handleImport = new ImportMemoryWin32HandleInfoKHR + { + PNext = &dedicatedAllocation, + SType = StructureType.ImportMemoryWin32HandleInfoKhr, + HandleType = ExternalMemoryHandleTypeFlags.D3D11TextureKmtBit, + Handle = _win32ShareHandle, + }; + } + + var memoryAllocateInfo = new MemoryAllocateInfo + { + PNext = + exportable ? RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? &handleImport : &fdExport : null, + SType = StructureType.MemoryAllocateInfo, + AllocationSize = memoryRequirements.Size, + MemoryTypeIndex = (uint)VulkanMemoryHelper.FindSuitableMemoryTypeIndex( + Api, + _physicalDevice, + memoryRequirements.MemoryTypeBits, MemoryPropertyFlags.MemoryPropertyDeviceLocalBit) + }; + + Api.AllocateMemory(_device, memoryAllocateInfo, null, + out var imageMemory).ThrowOnError(); + + _imageMemory = imageMemory; + + + MemorySize = memoryRequirements.Size; + + Api.BindImageMemory(_device, InternalHandle.Value, _imageMemory, 0).ThrowOnError(); + var componentMapping = new ComponentMapping( + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity, + ComponentSwizzle.Identity); + + AspectFlags = ImageAspectFlags.ImageAspectColorBit; + + var subresourceRange = new ImageSubresourceRange(AspectFlags, 0, MipLevels, 0, 1); + + var imageViewCreateInfo = new ImageViewCreateInfo + { + SType = StructureType.ImageViewCreateInfo, + Image = InternalHandle.Value, + ViewType = ImageViewType.ImageViewType2D, + Format = Format, + Components = componentMapping, + SubresourceRange = subresourceRange + }; + + Api + .CreateImageView(_device, imageViewCreateInfo, null, out var imageView) + .ThrowOnError(); + + _imageView = imageView; + + _currentLayout = ImageLayout.Undefined; + + TransitionLayout(ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessNoneKhr); + } + + public int ExportFd() + { + if (!Api.TryGetDeviceExtension(_instance, _device, out var ext)) + throw new InvalidOperationException(); + var info = new MemoryGetFdInfoKHR + { + Memory = _imageMemory, + SType = StructureType.MemoryGetFDInfoKhr, + HandleType = ExternalMemoryHandleTypeFlags.OpaqueFDBit + }; + ext.GetMemoryF(_device, info, out var fd).ThrowOnError(); + return fd; + } + + public IPlatformHandle Export() => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? + new PlatformHandle(_win32ShareHandle, + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle) : + new PlatformHandle(new IntPtr(ExportFd()), + KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor); + + public ImageTiling Tiling => ImageTiling.Optimal; + + + + internal void TransitionLayout(CommandBuffer commandBuffer, + ImageLayout fromLayout, AccessFlags fromAccessFlags, + ImageLayout destinationLayout, AccessFlags destinationAccessFlags) + { + VulkanMemoryHelper.TransitionLayout(Api, commandBuffer, InternalHandle.Value, + fromLayout, + fromAccessFlags, + destinationLayout, destinationAccessFlags, + MipLevels); + + _currentLayout = destinationLayout; + _currentAccessFlags = destinationAccessFlags; + } + + internal void TransitionLayout(CommandBuffer commandBuffer, + ImageLayout destinationLayout, AccessFlags destinationAccessFlags) + => TransitionLayout(commandBuffer, _currentLayout, _currentAccessFlags, destinationLayout, + destinationAccessFlags); + + + internal void TransitionLayout(ImageLayout destinationLayout, AccessFlags destinationAccessFlags) + { + var commandBuffer = _commandBufferPool.CreateCommandBuffer(); + commandBuffer.BeginRecording(); + TransitionLayout(commandBuffer.InternalHandle, destinationLayout, destinationAccessFlags); + commandBuffer.EndRecording(); + commandBuffer.Submit(); + } + + public void TransitionLayout(uint destinationLayout, uint destinationAccessFlags) + { + TransitionLayout((ImageLayout)destinationLayout, (AccessFlags)destinationAccessFlags); + } + + public unsafe void Dispose() + { + Api.DestroyImageView(_device, _imageView.Value, null); + Api.DestroyImage(_device, InternalHandle.Value, null); + Api.FreeMemory(_device, _imageMemory, null); + + _imageView = default; + InternalHandle = default; + _imageMemory = default; + } + + public void SaveTexture(string path) + { + _vk.GrContext.ResetContext(); + var _image = this; + var imageInfo = new GRVkImageInfo() + { + CurrentQueueFamily = _vk.QueueFamilyIndex, + Format = (uint)_image.Format, + Image = _image.Handle, + ImageLayout = (uint)_image.CurrentLayout, + ImageTiling = (uint)_image.Tiling, + ImageUsageFlags = (uint)_image.UsageFlags, + LevelCount = _image.MipLevels, + SampleCount = 1, + Protected = false, + Alloc = new GRVkAlloc() + { + Memory = _image.MemoryHandle, Flags = 0, Offset = 0, Size = _image.MemorySize + } + }; + + using (var backendTexture = new GRBackendRenderTarget(_image.Size.Width, _image.Size.Height, 1, + imageInfo)) + using (var surface = SKSurface.Create(_vk.GrContext, backendTexture, + GRSurfaceOrigin.TopLeft, + SKColorType.Rgba8888, SKColorSpace.CreateSrgb())) + { + using var snap = surface.Snapshot(); + using var encoded = snap.Encode(); + using (var s = File.Create(path)) + encoded.SaveTo(s); + } + } + } diff --git a/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs b/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs new file mode 100644 index 0000000000..f6778610dc --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanMemoryHelper.cs @@ -0,0 +1,59 @@ +using Silk.NET.Vulkan; + +namespace GpuInterop.VulkanDemo; + +internal static class VulkanMemoryHelper +{ + internal static int FindSuitableMemoryTypeIndex(Vk api, PhysicalDevice physicalDevice, uint memoryTypeBits, + MemoryPropertyFlags flags) + { + api.GetPhysicalDeviceMemoryProperties(physicalDevice, out var properties); + + for (var i = 0; i < properties.MemoryTypeCount; i++) + { + var type = properties.MemoryTypes[i]; + + if ((memoryTypeBits & (1 << i)) != 0 && type.PropertyFlags.HasFlag(flags)) return i; + } + + return -1; + } + + internal static unsafe void TransitionLayout( + Vk api, + CommandBuffer commandBuffer, + Image image, + ImageLayout sourceLayout, + AccessFlags sourceAccessMask, + ImageLayout destinationLayout, + AccessFlags destinationAccessMask, + uint mipLevels) + { + var subresourceRange = new ImageSubresourceRange(ImageAspectFlags.ImageAspectColorBit, 0, mipLevels, 0, 1); + + var barrier = new ImageMemoryBarrier + { + SType = StructureType.ImageMemoryBarrier, + SrcAccessMask = sourceAccessMask, + DstAccessMask = destinationAccessMask, + OldLayout = sourceLayout, + NewLayout = destinationLayout, + SrcQueueFamilyIndex = Vk.QueueFamilyIgnored, + DstQueueFamilyIndex = Vk.QueueFamilyIgnored, + Image = image, + SubresourceRange = subresourceRange + }; + + api.CmdPipelineBarrier( + commandBuffer, + PipelineStageFlags.PipelineStageAllCommandsBit, + PipelineStageFlags.PipelineStageAllCommandsBit, + 0, + 0, + null, + 0, + null, + 1, + barrier); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs b/samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs new file mode 100644 index 0000000000..279c313a27 --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanSemaphorePair.cs @@ -0,0 +1,58 @@ +using System; +using Silk.NET.Vulkan; +using Silk.NET.Vulkan.Extensions.KHR; +using SilkNetDemo; + +namespace GpuInterop.VulkanDemo; + +class VulkanSemaphorePair : IDisposable +{ + private readonly VulkanContext _resources; + + public unsafe VulkanSemaphorePair(VulkanContext resources, bool exportable) + { + _resources = resources; + + var semaphoreExportInfo = new ExportSemaphoreCreateInfo + { + SType = StructureType.ExportSemaphoreCreateInfo, + HandleTypes = ExternalSemaphoreHandleTypeFlags.OpaqueFDBit + }; + + var semaphoreCreateInfo = new SemaphoreCreateInfo + { + SType = StructureType.SemaphoreCreateInfo, + PNext = exportable ? &semaphoreExportInfo : null + }; + + resources.Api.CreateSemaphore(resources.Device, semaphoreCreateInfo, null, out var semaphore).ThrowOnError(); + ImageAvailableSemaphore = semaphore; + + resources.Api.CreateSemaphore(resources.Device, semaphoreCreateInfo, null, out semaphore).ThrowOnError(); + RenderFinishedSemaphore = semaphore; + } + + public int ExportFd(bool renderFinished) + { + if (!_resources.Api.TryGetDeviceExtension(_resources.Instance, _resources.Device, + out var ext)) + throw new InvalidOperationException(); + var info = new SemaphoreGetFdInfoKHR() + { + SType = StructureType.SemaphoreGetFDInfoKhr, + Semaphore = renderFinished ? RenderFinishedSemaphore : ImageAvailableSemaphore, + HandleType = ExternalSemaphoreHandleTypeFlags.OpaqueFDBit + }; + ext.GetSemaphoreF(_resources.Device, info, out var fd).ThrowOnError(); + return fd; + } + + internal Semaphore ImageAvailableSemaphore { get; } + internal Semaphore RenderFinishedSemaphore { get; } + + public unsafe void Dispose() + { + _resources.Api.DestroySemaphore(_resources.Device, ImageAvailableSemaphore, null); + _resources.Api.DestroySemaphore(_resources.Device, RenderFinishedSemaphore, null); + } +} \ No newline at end of file diff --git a/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs b/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs new file mode 100644 index 0000000000..325c815ccb --- /dev/null +++ b/samples/GpuInterop/VulkanDemo/VulkanSwapchain.cs @@ -0,0 +1,154 @@ +using System; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Vulkan; +using Metsys.Bson; +using Silk.NET.Vulkan; +using SkiaSharp; + +namespace GpuInterop.VulkanDemo; + +class VulkanSwapchain : SwapchainBase +{ + private readonly VulkanContext _vk; + + public VulkanSwapchain(VulkanContext vk, ICompositionGpuInterop interop, CompositionDrawingSurface target) : base(interop, target) + { + _vk = vk; + } + + protected override VulkanSwapchainImage CreateImage(PixelSize size) + { + return new VulkanSwapchainImage(_vk, size, Interop, Target); + } + + public IDisposable BeginDraw(PixelSize size, out VulkanImage image) + { + _vk.Pool.FreeUsedCommandBuffers(); + var rv = BeginDrawCore(size, out var swapchainImage); + image = swapchainImage.Image; + return rv; + } +} + +class VulkanSwapchainImage : ISwapchainImage +{ + private readonly VulkanContext _vk; + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _target; + private readonly VulkanImage _image; + private readonly VulkanSemaphorePair _semaphorePair; + private ICompositionImportedGpuSemaphore? _availableSemaphore, _renderCompletedSemaphore; + private ICompositionImportedGpuImage? _importedImage; + private Task? _lastPresent; + public VulkanImage Image => _image; + private bool _initial = true; + + public VulkanSwapchainImage(VulkanContext vk, PixelSize size, ICompositionGpuInterop interop, CompositionDrawingSurface target) + { + _vk = vk; + _interop = interop; + _target = target; + Size = size; + _image = new VulkanImage(vk, (uint)Format.R8G8B8A8Unorm, size, true); + _semaphorePair = new VulkanSemaphorePair(vk, true); + } + + public async ValueTask DisposeAsync() + { + if (LastPresent != null) + await LastPresent; + if (_importedImage != null) + await _importedImage.DisposeAsync(); + if (_availableSemaphore != null) + await _availableSemaphore.DisposeAsync(); + if (_renderCompletedSemaphore != null) + await _renderCompletedSemaphore.DisposeAsync(); + _semaphorePair.Dispose(); + _image.Dispose(); + } + + public PixelSize Size { get; } + + public Task? LastPresent => _lastPresent; + + public void BeginDraw() + { + var buffer = _vk.Pool.CreateCommandBuffer(); + buffer.BeginRecording(); + + _image.TransitionLayout(buffer.InternalHandle, + ImageLayout.Undefined, AccessFlags.None, + ImageLayout.ColorAttachmentOptimal, AccessFlags.AccessColorAttachmentReadBit); + + if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + buffer.Submit(null,null,null, null, new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo + { + AcquireKey = 0, + DeviceMemory = _image.DeviceMemory + }); + else if (_initial) + { + _initial = false; + buffer.Submit(); + } + else + buffer.Submit(new[] { _semaphorePair.ImageAvailableSemaphore }, + new[] + { + PipelineStageFlags.AllGraphicsBit + }); + } + + + + public void Present() + { + var buffer = _vk.Pool.CreateCommandBuffer(); + buffer.BeginRecording(); + _image.TransitionLayout(buffer.InternalHandle, ImageLayout.TransferSrcOptimal, AccessFlags.TransferWriteBit); + + + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + buffer.Submit(null, null, null, null, + new VulkanCommandBufferPool.VulkanCommandBuffer.KeyedMutexSubmitInfo + { + DeviceMemory = _image.DeviceMemory, ReleaseKey = 1 + }); + } + else + buffer.Submit(null, null, new[] { _semaphorePair.RenderFinishedSemaphore }); + + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _availableSemaphore ??= _interop.ImportSemaphore(new PlatformHandle( + new IntPtr(_semaphorePair.ExportFd(false)), + KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor)); + + _renderCompletedSemaphore ??= _interop.ImportSemaphore(new PlatformHandle( + new IntPtr(_semaphorePair.ExportFd(true)), + KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor)); + } + + _importedImage ??= _interop.ImportImage(_image.Export(), + new PlatformGraphicsExternalImageProperties + { + Format = PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm, + Width = Size.Width, + Height = Size.Height, + MemorySize = _image.MemorySize + }); + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + _lastPresent = _target.UpdateWithKeyedMutexAsync(_importedImage, 1, 0); + else + _lastPresent = _target.UpdateWithSemaphoresAsync(_importedImage, _renderCompletedSemaphore, _availableSemaphore); + } +} diff --git a/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs b/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs new file mode 100644 index 0000000000..5b06651b5f --- /dev/null +++ b/src/Avalonia.Base/Platform/IExternalObjectsRenderInterfaceContextFeature.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Avalonia.Metadata; +using Avalonia.Rendering.Composition; + +namespace Avalonia.Platform; + +[Unstable] +public interface IExternalObjectsRenderInterfaceContextFeature +{ + /// + /// Returns the list of image handle types supported by the current GPU backend, see + /// + IReadOnlyList SupportedImageHandleTypes { get; } + + /// + /// Returns the list of semaphore types supported by the current GPU backend, see + /// + IReadOnlyList SupportedSemaphoreTypes { get; } + + IPlatformRenderInterfaceImportedImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties); + + IPlatformRenderInterfaceImportedImage ImportImage(ICompositionImportableSharedGpuContextImage image); + + IPlatformRenderInterfaceImportedSemaphore ImportSemaphore(IPlatformHandle handle); + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + public byte[]? DeviceUuid { get; } + public byte[]? DeviceLuid { get; } +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedObject : IDisposable +{ + +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedImage : IPlatformRenderInterfaceImportedObject +{ + IBitmapImpl SnapshotWithKeyedMutex(uint acquireIndex, uint releaseIndex); + + IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore, + IPlatformRenderInterfaceImportedSemaphore signalSemaphore); + + IBitmapImpl SnapshotWithAutomaticSync(); +} + +[Unstable] +public interface IPlatformRenderInterfaceImportedSemaphore : IPlatformRenderInterfaceImportedObject +{ + +} diff --git a/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs index b6464dea58..66d4f083c2 100644 --- a/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs +++ b/src/Avalonia.Base/Platform/IOptionalFeatureProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; namespace Avalonia.Platform; @@ -15,4 +16,12 @@ public static class OptionalFeatureProviderExtensions { public static T? TryGetFeature(this IOptionalFeatureProvider provider) where T : class => (T?)provider.TryGetFeature(typeof(T)); -} \ No newline at end of file + + public static bool TryGetFeature(this IOptionalFeatureProvider provider, [MaybeNullWhen(false)] out T rv) + where T : class + { + rv = provider.TryGetFeature(); + return rv != null; + } + +} diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 1828f24aff..89bf047401 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -210,5 +210,10 @@ namespace Avalonia.Platform /// /// An . IRenderTarget CreateRenderTarget(IEnumerable surfaces); + + /// + /// Indicates that the context is no longer usable. This method should be thread-safe + /// + bool IsLost { get; } } } diff --git a/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs new file mode 100644 index 0000000000..cad4ab2051 --- /dev/null +++ b/src/Avalonia.Base/Platform/PlatformGraphicsExternalMemory.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Avalonia.Platform; + +public struct PlatformGraphicsExternalImageProperties +{ + public int Width { get; set; } + public int Height { get; set; } + public PlatformGraphicsExternalImageFormat Format { get; set; } + public ulong MemorySize { get; set; } + public ulong MemoryOffset { get; set; } + public bool TopLeftOrigin { get; set; } +} + +public enum PlatformGraphicsExternalImageFormat +{ + R8G8B8A8UNorm, + B8G8R8A8UNorm +} + +/// +/// Describes various GPU memory handle types that are currently supported by Avalonia graphics backends +/// +public static class KnownPlatformGraphicsExternalImageHandleTypes +{ + /// + /// An DXGI global shared handle returned by IDXGIResource::GetSharedHandle D3D11_RESOURCE_MISC_SHARED or D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX flag. + /// The handle does not own the reference to the underlying video memory, so the provider should make sure that the resource is valid until + /// the handle has been successfully imported + /// + public const string D3D11TextureGlobalSharedHandle = nameof(D3D11TextureGlobalSharedHandle); + /// + /// A DXGI NT handle returned by IDXGIResource1::CreateSharedHandle for a texture created with D3D11_RESOURCE_MISC_SHARED_NTHANDLE or flag + /// + public const string D3D11TextureNtHandle = nameof(D3D11TextureNtHandle); + /// + /// A POSIX file descriptor that's exported by Vulkan using VK_EXTERNAL_MEMORY_HANDLE_TYPE_OPAQUE_FD_BIT or in a compatible way + /// + public const string VulkanOpaquePosixFileDescriptor = nameof(VulkanOpaquePosixFileDescriptor); +} + +/// +/// Describes various GPU semaphore handle types that are currently supported by Avalonia graphics backends +/// +public static class KnownPlatformGraphicsExternalSemaphoreHandleTypes +{ + /// + /// A POSIX file descriptor that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_FD_BIT or in a compatible way + /// + public const string VulkanOpaquePosixFileDescriptor = nameof(VulkanOpaquePosixFileDescriptor); + + /// + /// A NT handle that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_BIT or in a compatible way + /// + public const string VulkanOpaqueNtHandle = nameof(VulkanOpaqueNtHandle); + + // A global shared handle that's been exported by Vulkan using VK_EXTERNAL_SEMAPHORE_HANDLE_TYPE_OPAQUE_WIN32_KMT_BIT or in a compatible way + public const string VulkanOpaqueKmtHandle = nameof(VulkanOpaqueKmtHandle); + + /// A DXGI NT handle returned by ID3D12Device::CreateSharedHandle or ID3D11Fence::CreateSharedHandle + public const string Direct3D12FenceNtHandle = nameof(Direct3D12FenceNtHandle); +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index b2080aeb87..a2209db09f 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -28,7 +28,7 @@ public class CompositingRenderer : IRendererWithCompositor private HashSet _dirty = new(); private HashSet _recalculateChildren = new(); private bool _queuedUpdate; - private Action _update; + private Action _update; private bool _updating; internal CompositionTarget CompositionTarget; @@ -70,7 +70,7 @@ public class CompositingRenderer : IRendererWithCompositor if(_queuedUpdate) return; _queuedUpdate = true; - _compositor.InvokeBeforeNextCommit(_update); + _compositor.RequestCompositionUpdate(_update); } /// @@ -265,7 +265,7 @@ public class CompositingRenderer : IRendererWithCompositor SceneInvalidated?.Invoke(this, new SceneInvalidatedEventArgs(_root, new Rect(_root.ClientSize))); } - private void Update(Task batchCompletion) + private void Update() { if(_updating) return; diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs new file mode 100644 index 0000000000..2f7e261250 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs @@ -0,0 +1,60 @@ +using System; +using System.Threading.Tasks; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +public class CompositionDrawingSurface : CompositionSurface +{ + internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server; + internal CompositionDrawingSurface(Compositor compositor) : base(compositor, new ServerCompositionDrawingSurface(compositor.Server)) + { + } + + /// + /// Updates the surface contents using an imported memory image using a keyed mutex as the means of synchronization + /// + /// GPU image with new surface contents + /// The mutex key to wait for before accessing the image + /// The mutex key to release for after accessing the image + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateWithKeyedMutexAsync(ICompositionImportedGpuImage image, uint acquireIndex, uint releaseIndex) + { + var img = (CompositionImportedGpuImage)image; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithKeyedMutex(img, acquireIndex, releaseIndex)); + } + + /// + /// Updates the surface contents using an imported memory image using a semaphore pair as the means of synchronization + /// + /// GPU image with new surface contents + /// The semaphore to wait for before accessing the image + /// The semaphore to signal after accessing the image + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateWithSemaphoresAsync(ICompositionImportedGpuImage image, + ICompositionImportedGpuSemaphore waitForSemaphore, + ICompositionImportedGpuSemaphore signalSemaphore) + { + var img = (CompositionImportedGpuImage)image; + var wait = (CompositionImportedGpuSemaphore)waitForSemaphore; + var signal = (CompositionImportedGpuSemaphore)signalSemaphore; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithSemaphores(img, wait, signal)); + } + + /// + /// Updates the surface contents using an unspecified automatic means of synchronization + /// provided by the underlying platform + /// + /// GPU image with new surface contents + /// A task that completes when update operation is completed and user code is free to destroy or dispose the image + public Task UpdateAsync(ICompositionImportedGpuImage image) + { + var img = (CompositionImportedGpuImage)image; + return Compositor.InvokeServerJobAsync(() => Server.UpdateWithAutomaticSync(img)); + } + + ~CompositionDrawingSurface() + { + Dispose(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs b/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs new file mode 100644 index 0000000000..ce728f86a2 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionExternalMemory.cs @@ -0,0 +1,142 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Metadata; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition; +public interface ICompositionGpuInterop +{ + /// + /// Returns the list of image handle types supported by the current GPU backend, see + /// + IReadOnlyList SupportedImageHandleTypes { get; } + + /// + /// Returns the list of semaphore types supported by the current GPU backend, see + /// + IReadOnlyList SupportedSemaphoreTypes { get; } + + /// + /// Returns the supported ways to synchronize access to the imported GPU image + /// + /// + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + + /// + /// Asynchronously imports a texture. The returned object is immediately usable. + /// + ICompositionImportedGpuImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties); + + /// + /// Asynchronously imports a texture. The returned object is immediately usable. + /// If import operation fails, the caller is responsible for destroying the handle + /// + /// An image that belongs to the same GPU context or the same GPU context sharing group as one used by compositor + ICompositionImportedGpuImage ImportImage(ICompositionImportableSharedGpuContextImage image); + + /// + /// Asynchronously imports a semaphore object. The returned object is immediately usable. + /// If import operation fails, the caller is responsible for destroying the handle + /// + ICompositionImportedGpuSemaphore ImportSemaphore(IPlatformHandle handle); + + /// + /// Asynchronously imports a semaphore object. The returned object is immediately usable. + /// + /// A semaphore that belongs to the same GPU context or the same GPU context sharing group as one used by compositor + ICompositionImportedGpuImage ImportSemaphore(ICompositionImportableSharedGpuContextSemaphore image); + + /// + /// Indicates if the device context this instance is associated with is no longer available + /// + public bool IsLost { get; } + + /// + /// The LUID of the graphics adapter used by the compositor + /// + public byte[]? DeviceLuid { get; set; } + + /// + /// The UUID of the graphics adapter used by the compositor + /// + public byte[]? DeviceUuid { get; set; } +} + +[Flags] +public enum CompositionGpuImportedImageSynchronizationCapabilities +{ + /// + /// Pre-render and after-render semaphores must be provided alongside with the image + /// + Semaphores = 1, + /// + /// Image must be created with D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX or in other compatible way + /// + KeyedMutex = 2, + /// + /// Synchronization and ordering is somehow handled by the underlying platform + /// + Automatic = 4 +} + +/// +/// An imported GPU object that's usable by composition APIs +/// +public interface ICompositionGpuImportedObject : IAsyncDisposable +{ + /// + /// Tracks the import status of the object. Once the task is completed, + /// the user code is allowed to free the resource owner in case when a non-owning + /// sharing handle was used + /// + Task ImportCompeted { get; } + /// + /// Indicates if the device context this instance is associated with is no longer available + /// + bool IsLost { get; } +} + +/// +/// An imported GPU image object that's usable by composition APIs +/// +[NotClientImplementable] +public interface ICompositionImportedGpuImage : ICompositionGpuImportedObject +{ + +} + +/// +/// An imported GPU semaphore object that's usable by composition APIs +/// +[NotClientImplementable] +public interface ICompositionImportedGpuSemaphore : ICompositionGpuImportedObject +{ + +} + +/// +/// An GPU object descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextObject : IDisposable +{ +} + +/// +/// An GPU image descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextImage : IDisposable +{ +} + +/// +/// An GPU semaphore descriptor obtained from a context from the same share group as one used by the compositor +/// +[NotClientImplementable] +public interface ICompositionImportableSharedGpuContextSemaphore : IDisposable +{ +} + diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs b/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs new file mode 100644 index 0000000000..1643ec6e8d --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionInterop.cs @@ -0,0 +1,150 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition; + +internal class CompositionInterop : ICompositionGpuInterop +{ + private readonly Compositor _compositor; + private readonly IPlatformRenderInterfaceContext _context; + private readonly IExternalObjectsRenderInterfaceContextFeature _externalObjects; + + + public CompositionInterop( + Compositor compositor, + IExternalObjectsRenderInterfaceContextFeature externalObjects) + { + _compositor = compositor; + _context = compositor.Server.RenderInterface.Value; + DeviceLuid = externalObjects.DeviceLuid; + DeviceUuid = externalObjects.DeviceUuid; + _externalObjects = externalObjects; + } + + public IReadOnlyList SupportedImageHandleTypes => _externalObjects.SupportedImageHandleTypes; + public IReadOnlyList SupportedSemaphoreTypes => _externalObjects.SupportedSemaphoreTypes; + + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + => _externalObjects.GetSynchronizationCapabilities(imageHandleType); + + public ICompositionImportedGpuImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties) + => new CompositionImportedGpuImage(_compositor, _context, _externalObjects, + () => _externalObjects.ImportImage(handle, properties)); + + public ICompositionImportedGpuImage ImportImage(ICompositionImportableSharedGpuContextImage image) + { + return new CompositionImportedGpuImage(_compositor, _context, _externalObjects, + () => _externalObjects.ImportImage(image)); + } + + public ICompositionImportedGpuSemaphore ImportSemaphore(IPlatformHandle handle) + => new CompositionImportedGpuSemaphore(handle, _compositor, _context, _externalObjects); + + public ICompositionImportedGpuImage ImportSemaphore(ICompositionImportableSharedGpuContextSemaphore image) + { + throw new System.NotSupportedException(); + } + + public bool IsLost { get; } + public byte[]? DeviceLuid { get; set; } + public byte[]? DeviceUuid { get; set; } +} + +abstract class CompositionGpuImportedObjectBase : ICompositionGpuImportedObject +{ + protected Compositor Compositor { get; } + public IPlatformRenderInterfaceContext Context { get; } + public IExternalObjectsRenderInterfaceContextFeature Feature { get; } + + public CompositionGpuImportedObjectBase(Compositor compositor, + IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature) + { + Compositor = compositor; + Context = context; + Feature = feature; + + ImportCompeted = Compositor.InvokeServerJobAsync(Import); + } + + protected abstract void Import(); + public abstract void Dispose(); + + public Task ImportCompeted { get; } + public bool IsLost => Context.IsLost; + + public ValueTask DisposeAsync() => new(Compositor.InvokeServerJobAsync(() => + { + if (ImportCompeted.Status == TaskStatus.RanToCompletion) + Dispose(); + })); +} + +class CompositionImportedGpuImage : CompositionGpuImportedObjectBase, ICompositionImportedGpuImage +{ + private readonly Func _importer; + private IPlatformRenderInterfaceImportedImage? _image; + + public CompositionImportedGpuImage(Compositor compositor, + IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature, + Func importer): base(compositor, context, feature) + { + _importer = importer; + } + + protected override void Import() + { + using (Compositor.Server.RenderInterface.EnsureCurrent()) + { + // The original context was lost and the new one might have different capabilities + if (Context != Compositor.Server.RenderInterface.Value) + throw new PlatformGraphicsContextLostException(); + _image = _importer(); + } + } + + public IPlatformRenderInterfaceImportedImage Image => + _image ?? throw new ObjectDisposedException(nameof(CompositionImportedGpuImage)); + + public bool IsUsable => _image != null && Compositor.Server.RenderInterface.Value == Context; + + public override void Dispose() + { + _image?.Dispose(); + _image = null!; + } +} + +class CompositionImportedGpuSemaphore : CompositionGpuImportedObjectBase, ICompositionImportedGpuSemaphore +{ + private readonly IPlatformHandle _handle; + private IPlatformRenderInterfaceImportedSemaphore? _semaphore; + + public CompositionImportedGpuSemaphore(IPlatformHandle handle, + Compositor compositor, IPlatformRenderInterfaceContext context, + IExternalObjectsRenderInterfaceContextFeature feature) : base(compositor, context, feature) + { + _handle = handle; + } + + public IPlatformRenderInterfaceImportedSemaphore Semaphore => + _semaphore ?? throw new ObjectDisposedException(nameof(CompositionImportedGpuSemaphore)); + + + public bool IsUsable => _semaphore != null && Compositor.Server.RenderInterface.Value == Context; + + protected override void Import() + { + _semaphore = Feature.ImportSemaphore(_handle); + } + + public override void Dispose() + { + _semaphore?.Dispose(); + _semaphore = null; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs new file mode 100644 index 0000000000..0edd8ac732 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionSurface.cs @@ -0,0 +1,10 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +public class CompositionSurface : CompositionObject +{ + internal CompositionSurface(Compositor compositor, ServerObject server) : base(compositor, server) + { + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 2b1b3f461f..6dba18704f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -35,4 +35,8 @@ public partial class Compositor new(this, new ServerCompositionSolidColorVisual(Server)); public CompositionCustomVisual CreateCustomVisual(CompositionCustomVisualHandler handler) => new(this, handler); -} \ No newline at end of file + + public CompositionSurfaceVisual CreateSurfaceVisual() => new(this, new ServerCompositionSurfaceVisual(_server)); + + public CompositionDrawingSurface CreateDrawingSurface() => new(this); +} diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 7fc5487171..bdcbe65403 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -30,7 +30,7 @@ namespace Avalonia.Rendering.Composition private BatchStreamObjectPool _batchObjectPool = new(); private BatchStreamMemoryPool _batchMemoryPool = new(); private List _objectsForSerialization = new(); - private Queue> _invokeBeforeCommit = new(); + private Queue _invokeBeforeCommitWrite = new(), _invokeBeforeCommitRead = new(); internal ServerCompositor Server => _server; private Task? _pendingBatch; private readonly object _pendingBatchLock = new(); @@ -77,16 +77,30 @@ namespace Avalonia.Rendering.Composition return _nextCommit.Task; } - + internal Task Commit() + { + try + { + return CommitCore(); + } + finally + { + if (_invokeBeforeCommitWrite.Count > 0) + RequestCommitAsync(); + } + } + + Task CommitCore() { Dispatcher.UIThread.VerifyAccess(); using var noPump = NonPumpingLockHelper.Use(); _nextCommit ??= new TaskCompletionSource(); - while (_invokeBeforeCommit.Count > 0) - _invokeBeforeCommit.Dequeue()(_nextCommit.Task); + (_invokeBeforeCommitRead, _invokeBeforeCommitWrite) = (_invokeBeforeCommitWrite, _invokeBeforeCommitRead); + while (_invokeBeforeCommitRead.Count > 0) + _invokeBeforeCommitRead.Dequeue()(); var batch = new Batch(_nextCommit); @@ -109,6 +123,7 @@ namespace Avalonia.Rendering.Composition writer.WriteObject(job); writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker); } + _pendingServerCompositorJobs.Clear(); } batch.CommittedAt = Server.Clock.Elapsed; @@ -138,34 +153,73 @@ namespace Avalonia.Rendering.Composition RequestCommitAsync(); } - internal void InvokeBeforeNextCommit(Action action) + /// + /// Enqueues a callback to be called before the next scheduled commit. + /// If there is no scheduled commit it automatically schedules one + /// This is useful for updating your composition tree objects after binding + /// and layout passes have completed + /// + public void RequestCompositionUpdate(Action action) { Dispatcher.UIThread.VerifyAccess(); - _invokeBeforeCommit.Enqueue(action); + _invokeBeforeCommitWrite.Enqueue(action); RequestCommitAsync(); } - /// - /// Attempts to query for a feature from the platform render interface - /// - public ValueTask TryGetRenderInterfaceFeature(Type featureType) + internal void PostServerJob(Action job) { - var tcs = new TaskCompletionSource(); - _pendingServerCompositorJobs.Add(() => + Dispatcher.UIThread.VerifyAccess(); + _pendingServerCompositorJobs.Add(job); + RequestCommitAsync(); + } + + internal Task InvokeServerJobAsync(Action job) => + InvokeServerJobAsync(() => + { + job(); + return null; + }); + + internal Task InvokeServerJobAsync(Func job) + { + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + PostServerJob(() => { try { - using (Server.RenderInterface.EnsureCurrent()) - { - tcs.TrySetResult(Server.RenderInterface.Value.TryGetFeature(featureType)); - } + tcs.SetResult(job()); } catch (Exception e) { tcs.TrySetException(e); } }); - return new ValueTask(tcs.Task); + return tcs.Task; } + + /// + /// Attempts to query for a feature from the platform render interface + /// + public ValueTask TryGetRenderInterfaceFeature(Type featureType) => + new(InvokeServerJobAsync(() => + { + using (Server.RenderInterface.EnsureCurrent()) + { + return Server.RenderInterface.Value.TryGetFeature(featureType); + } + })); + + public ValueTask TryGetCompositionGpuInterop() => + new(InvokeServerJobAsync(() => + { + using (Server.RenderInterface.EnsureCurrent()) + { + var feature = Server.RenderInterface.Value + .TryGetFeature(); + if (feature == null) + return null; + return new CompositionInterop(this, feature); + } + })); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs new file mode 100644 index 0000000000..9c6c78c1ad --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawingSurface.cs @@ -0,0 +1,74 @@ +using System; +using System.Runtime.ExceptionServices; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionDrawingSurface : ServerCompositionSurface, IDisposable +{ + private IRef? _bitmap; + private IPlatformRenderInterfaceContext? _createdWithContext; + public override IRef? Bitmap + { + get + { + // Failsafe to avoid consuming an image imported with a different context + if (Compositor.RenderInterface.Value != _createdWithContext) + return null; + return _bitmap; + } + } + + public ServerCompositionDrawingSurface(ServerCompositor compositor) : base(compositor) + { + } + + void PerformSanityChecks(CompositionImportedGpuImage image) + { + // Failsafe to avoid consuming an image imported with a different context + if (!image.IsUsable) + throw new PlatformGraphicsContextLostException(); + + // This should never happen, but check for it anyway to avoid a deadlock + if (!image.ImportCompeted.IsCompleted) + throw new InvalidOperationException("The import operation is not completed yet"); + + // Rethrow the import here exception + if (image.ImportCompeted.IsFaulted) + image.ImportCompeted.GetAwaiter().GetResult(); + } + + void Update(IBitmapImpl newImage, IPlatformRenderInterfaceContext context) + { + _bitmap?.Dispose(); + _bitmap = RefCountable.Create(newImage); + _createdWithContext = context; + Changed?.Invoke(); + } + + public void UpdateWithAutomaticSync(CompositionImportedGpuImage image) + { + PerformSanityChecks(image); + Update(image.Image.SnapshotWithAutomaticSync(), image.Context); + } + + public void UpdateWithKeyedMutex(CompositionImportedGpuImage image, uint acquireIndex, uint releaseIndex) + { + PerformSanityChecks(image); + Update(image.Image.SnapshotWithKeyedMutex(acquireIndex, releaseIndex), image.Context); + } + + public void UpdateWithSemaphores(CompositionImportedGpuImage image, CompositionImportedGpuSemaphore wait, CompositionImportedGpuSemaphore signal) + { + PerformSanityChecks(image); + if (!wait.IsUsable || !signal.IsUsable) + throw new PlatformGraphicsContextLostException(); + Update(image.Image.SnapshotWithSemaphores(wait.Semaphore, signal.Semaphore), image.Context); + } + + public void Dispose() + { + _bitmap?.Dispose(); + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs index 32a99fa187..88f10ba642 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurface.cs @@ -1,11 +1,18 @@ // Special license applies License.md +using System; +using Avalonia.Platform; +using Avalonia.Utilities; + namespace Avalonia.Rendering.Composition.Server { - internal abstract class ServerCompositionSurface : ServerObject + internal abstract partial class ServerCompositionSurface : ServerObject { protected ServerCompositionSurface(ServerCompositor compositor) : base(compositor) { } + + public abstract IRef? Bitmap { get; } + public Action? Changed { get; set; } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs new file mode 100644 index 0000000000..c75ae8e631 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs @@ -0,0 +1,48 @@ +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositionSurfaceVisual +{ + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + if (Surface == null) + return; + if (Surface.Bitmap == null) + return; + var bmp = Surface.Bitmap.Item; + + //TODO: add a way to always render the whole bitmap instead of just assuming 96 DPI + canvas.DrawBitmap(Surface.Bitmap, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( + new Size(Size.X, Size.Y))); + } + + + private void OnSurfaceInvalidated() => ValuesInvalidated(); + + protected override void OnAttachedToRoot(ServerCompositionTarget target) + { + if (Surface != null) + Surface.Changed += OnSurfaceInvalidated; + base.OnAttachedToRoot(target); + } + + protected override void OnDetachedFromRoot(ServerCompositionTarget target) + { + if (Surface != null) + Surface.Changed -= OnSurfaceInvalidated; + base.OnDetachedFromRoot(target); + } + + partial void OnSurfaceChanged() + { + if (Surface != null) + Surface.Changed += OnSurfaceInvalidated; + } + + partial void OnSurfaceChanging() + { + if (Surface != null) + Surface.Changed -= OnSurfaceInvalidated; + } +} diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index e7405995f5..0492997200 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -22,7 +22,8 @@ namespace Avalonia.Rendering.Composition.Server { private readonly IRenderLoop _renderLoop; - private readonly Queue _batches = new Queue(); + private readonly Queue _batches = new Queue(); + private readonly Queue _receivedJobQueue = new(); public long LastBatchId { get; private set; } public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } @@ -75,7 +76,7 @@ namespace Avalonia.Rendering.Composition.Server var readObject = stream.ReadObject(); if (readObject == RenderThreadJobsStartMarker) { - ReadAndExecuteJobs(stream); + ReadServerJobs(stream); continue; } @@ -97,21 +98,24 @@ namespace Avalonia.Rendering.Composition.Server } } - void ReadAndExecuteJobs(BatchStreamReader reader) + void ReadServerJobs(BatchStreamReader reader) { object? readObject; while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker) - { - var job = (Action)readObject!; + _receivedJobQueue.Enqueue((Action)readObject!); + } + + void ExecuteServerJobs() + { + while(_receivedJobQueue.Count > 0) try { - job(); + _receivedJobQueue.Dequeue()(); } catch { // Ignore } - } } void CompletePendingBatches() @@ -160,6 +164,7 @@ namespace Avalonia.Rendering.Composition.Server try { RenderInterface.EnsureValidBackendContext(); + ExecuteServerJobs(); foreach (var t in _activeTargets) t.Render(); } diff --git a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs index 198b36564a..51a4ca1bf3 100644 --- a/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs +++ b/src/Avalonia.Base/Rendering/PlatformRenderInterfaceContextManager.cs @@ -50,6 +50,8 @@ public class PlatformRenderInterfaceContextManager } } + internal IPlatformGraphicsContext? GpuContext => _gpuContext?.Value; + public IDisposable EnsureCurrent() { EnsureValidBackendContext(); diff --git a/src/Avalonia.Base/Rendering/SwapchainBase.cs b/src/Avalonia.Base/Rendering/SwapchainBase.cs new file mode 100644 index 0000000000..ccfb704f00 --- /dev/null +++ b/src/Avalonia.Base/Rendering/SwapchainBase.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Rendering.Composition; + +namespace Avalonia.Rendering; + +/// +/// A helper class for composition-backed swapchains, should not be a public API yet +/// +abstract class SwapchainBase : IAsyncDisposable where TImage : class, ISwapchainImage +{ + protected ICompositionGpuInterop Interop { get; } + protected CompositionDrawingSurface Target { get; } + private List _pendingImages = new(); + + public SwapchainBase(ICompositionGpuInterop interop, CompositionDrawingSurface target) + { + Interop = interop; + Target = target; + } + + static bool IsBroken(TImage image) => image.LastPresent?.IsFaulted == true; + static bool IsReady(TImage image) => image.LastPresent == null || image.LastPresent.Status == TaskStatus.RanToCompletion; + + TImage? CleanupAndFindNextImage(PixelSize size) + { + TImage? firstFound = null; + var foundMultiple = false; + + for (var c = _pendingImages.Count - 1; c > -1; c--) + { + var image = _pendingImages[c]; + var ready = IsReady(image); + var matches = image.Size == size; + if (IsBroken(image) || (!matches && ready)) + { + image.DisposeAsync(); + _pendingImages.RemoveAt(c); + } + + if (matches && ready) + { + if (firstFound == null) + firstFound = image; + else + foundMultiple = true; + } + + } + + // We are making sure that there was at least one image of the same size in flight + // Otherwise we might encounter UI thread lockups + return foundMultiple ? firstFound : null; + } + + protected abstract TImage CreateImage(PixelSize size); + + protected IDisposable BeginDrawCore(PixelSize size, out TImage image) + { + var img = CleanupAndFindNextImage(size) ?? CreateImage(size); + + img.BeginDraw(); + _pendingImages.Remove(img); + image = img; + return Disposable.Create(() => + { + img.Present(); + _pendingImages.Add(img); + }); + } + + public async ValueTask DisposeAsync() + { + foreach (var img in _pendingImages) + await img.DisposeAsync(); + } +} + + +interface ISwapchainImage : IAsyncDisposable +{ + PixelSize Size { get; } + Task? LastPresent { get; } + void BeginDraw(); + void Present(); +} diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 6dfcb2e74d..a0dbf238dc 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -7,6 +7,8 @@ + + @@ -30,6 +32,9 @@ + + + @@ -46,4 +51,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 9bb4b34976..a12d876e64 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -49,6 +49,7 @@ namespace Avalonia.Headless public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, Geometry g1, Geometry g2) => throw new NotImplementedException(); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); + public bool IsLost => false; public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index 9b38c150fa..73493c69c0 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -15,7 +15,7 @@ - - + + diff --git a/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs b/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs new file mode 100644 index 0000000000..3db0aafd7b --- /dev/null +++ b/src/Avalonia.OpenGL/Controls/CompositionOpenGlSwapchain.cs @@ -0,0 +1,163 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; + +namespace Avalonia.OpenGL.Controls; + +internal class CompositionOpenGlSwapchain : SwapchainBase +{ + private readonly IGlContext _context; + private readonly IGlContextExternalObjectsFeature? _externalObjectsFeature; + private readonly IOpenGlTextureSharingRenderInterfaceContextFeature? _sharingFeature; + + public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target, + IOpenGlTextureSharingRenderInterfaceContextFeature sharingFeature + ) : base(interop, target) + { + _context = context; + _sharingFeature = sharingFeature; + } + + public CompositionOpenGlSwapchain(IGlContext context, ICompositionGpuInterop interop, CompositionDrawingSurface target, + IGlContextExternalObjectsFeature externalObjectsFeature) : base(interop, target) + { + _context = context; + _externalObjectsFeature = externalObjectsFeature; + } + + + + protected override IGlSwapchainImage CreateImage(PixelSize size) + { + if (_sharingFeature != null) + return new CompositionOpenGlSwapChainImage(_context, _sharingFeature, size, Interop, Target); + return new DxgiMutexOpenGlSwapChainImage(Interop, Target, _externalObjectsFeature!, size); + } + + public IDisposable BeginDraw(PixelSize size, out IGlTexture texture) + { + var rv = BeginDrawCore(size, out var tex); + texture = tex; + return rv; + } +} + +internal interface IGlTexture +{ + int TextureId { get; } + int InternalFormat { get; } + PixelSize Size { get; } +} + + +interface IGlSwapchainImage : ISwapchainImage, IGlTexture +{ + +} +internal class DxgiMutexOpenGlSwapChainImage : IGlSwapchainImage +{ + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _surface; + private readonly IGlExportableExternalImageTexture _texture; + private Task? _lastPresent; + private ICompositionImportedGpuImage? _imported; + + public DxgiMutexOpenGlSwapChainImage(ICompositionGpuInterop interop, CompositionDrawingSurface surface, + IGlContextExternalObjectsFeature externalObjects, PixelSize size) + { + _interop = interop; + _surface = surface; + _texture = externalObjects.CreateImage(KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle, + size, PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm); + } + public async ValueTask DisposeAsync() + { + // The texture is already sent to the compositor, so we need to wait for its attempts to use the texture + // before destroying it + if (_imported != null) + { + // No need to wait for import / LastPresent since calls are serialized on the compositor side anyway + try + { + await _imported.DisposeAsync(); + } + catch + { + // Ignore + } + } + _texture.Dispose(); + } + + public int TextureId => _texture.TextureId; + public int InternalFormat => _texture.InternalFormat; + public PixelSize Size => new(_texture.Properties.Width, _texture.Properties.Height); + public Task LastPresent => _lastPresent; + public void BeginDraw() => _texture.AcquireKeyedMutex(0); + + public void Present() + { + _texture.ReleaseKeyedMutex(1); + _imported ??= _interop.ImportImage(_texture.GetHandle(), _texture.Properties); + _lastPresent = _surface.UpdateWithKeyedMutexAsync(_imported, 1, 0); + } +} + +internal class CompositionOpenGlSwapChainImage : IGlSwapchainImage +{ + private readonly ICompositionGpuInterop _interop; + private readonly CompositionDrawingSurface _target; + private readonly ICompositionImportableOpenGlSharedTexture _texture; + private ICompositionImportedGpuImage? _imported; + + public CompositionOpenGlSwapChainImage( + IGlContext context, + IOpenGlTextureSharingRenderInterfaceContextFeature sharingFeature, + PixelSize size, + ICompositionGpuInterop interop, + CompositionDrawingSurface target) + { + _interop = interop; + _target = target; + _texture = sharingFeature.CreateSharedTextureForComposition(context, size); + } + + + public async ValueTask DisposeAsync() + { + // The texture is already sent to the compositor, so we need to wait for its attempts to use the texture + // before destroying it + if (_imported != null) + { + // No need to wait for import / LastPresent since calls are serialized on the compositor side anyway + try + { + await _imported.DisposeAsync(); + } + catch + { + // Ignore + } + } + + _texture.Dispose(); + } + + public int TextureId => _texture.TextureId; + public int InternalFormat => _texture.InternalFormat; + public PixelSize Size => _texture.Size; + public Task? LastPresent { get; private set; } + public void BeginDraw() + { + // No-op for texture sharing + } + + public void Present() + { + _imported ??= _interop.ImportImage(_texture); + LastPresent = _target.UpdateAsync(_imported); + } +} diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs index c62a3d8d2f..4d0663a12b 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs @@ -1,120 +1,53 @@ using System; +using System.Numerics; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Logging; -using Avalonia.Media; -using Avalonia.OpenGL.Imaging; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; using Avalonia.VisualTree; -using static Avalonia.OpenGL.GlConsts; - +using Avalonia.Platform; namespace Avalonia.OpenGL.Controls { public abstract class OpenGlControlBase : Control { - private IGlContext _context; - private int _fb, _depthBuffer; - private OpenGlBitmap _bitmap; - private IOpenGlBitmapAttachment _attachment; - private PixelSize _depthBufferSize; - + private CompositionSurfaceVisual _visual; + private Action _update; + private bool _updateQueued; private Task _initialization; - private IOpenGlTextureSharingRenderInterfaceContextFeature _feature; - - protected GlVersion GlVersion { get; private set; } - public sealed override void Render(DrawingContext context) - { - if(!EnsureInitialized()) - return; - - using (_context.MakeCurrent()) - { - _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); - EnsureTextureAttachment(); - EnsureDepthBufferAttachment(_context.GlInterface); - if(!CheckFramebufferStatus(_context.GlInterface)) - return; - - OnOpenGlRender(_context.GlInterface, _fb); - _attachment.Present(); - } - - context.DrawImage(_bitmap, new Rect(_bitmap.Size), new Rect(Bounds.Size)); - base.Render(context); - } + private OpenGlControlBaseResources? _resources; + private Compositor? _compositor; + protected GlVersion GlVersion => _resources?.Context.Version ?? default; - void EnsureTextureAttachment() + public OpenGlControlBase() { - _context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, _fb); - if (_bitmap == null || _attachment == null || _bitmap.PixelSize != GetPixelSize()) - { - _attachment?.Dispose(); - _attachment = null; - _bitmap?.Dispose(); - _bitmap = null; - _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); - _attachment = _bitmap.CreateFramebufferAttachment(_context); - } - } - - void EnsureDepthBufferAttachment(GlInterface gl) - { - var size = GetPixelSize(); - if (size == _depthBufferSize && _depthBuffer != 0) - return; - - gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); - if (_depthBuffer != 0) gl.DeleteRenderbuffer(_depthBuffer); - - _depthBuffer = gl.GenRenderbuffer(); - gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); - gl.RenderbufferStorage(GL_RENDERBUFFER, - GlVersion.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, - size.Width, size.Height); - gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthBuffer); - gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); + _update = Update; } void DoCleanup() { - if (_context != null) + if (_initialization is { Status: TaskStatus.RanToCompletion } && _resources != null) { - using (_context.MakeCurrent()) + try { - var gl = _context.GlInterface; - gl.ActiveTexture(GL_TEXTURE0); - gl.BindTexture(GL_TEXTURE_2D, 0); - gl.BindFramebuffer(GL_FRAMEBUFFER, 0); - if (_fb != 0) - gl.DeleteFramebuffer(_fb); - _fb = 0; - if (_depthBuffer != 0) - gl.DeleteRenderbuffer(_depthBuffer); - _depthBuffer = 0; - _attachment?.Dispose(); - _attachment = null; - _bitmap?.Dispose(); - _bitmap = null; - - try - { - if (_initialization is { Status: TaskStatus.RanToCompletion, Result: true }) - { - OnOpenGlDeinit(_context.GlInterface, _fb); - _initialization = null; - } - } - finally + using (_resources.Context.EnsureCurrent()) { - _context.Dispose(); - _context = null; + OnOpenGlDeinit(_resources.Context.GlInterface); } } + catch(Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to free user OpenGL resources: {exception}", e); + } } - _fb = _depthBuffer = 0; - _attachment = null; - _bitmap = null; - _feature = null; + ElementComposition.SetElementChildVisual(this, null); + _visual = null; + + _resources?.DisposeAsync(); + _resources = null; + _initialization = null; } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) @@ -123,95 +56,75 @@ namespace Avalonia.OpenGL.Controls base.OnDetachedFromVisualTree(e); } - private bool EnsureInitializedCore(IOpenGlTextureSharingRenderInterfaceContextFeature feature) + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { - try - { - _context = feature.CreateSharedContext(); - _feature = feature; - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); - return false; - } + base.OnAttachedToVisualTree(e); + _compositor = (this.GetVisualRoot()?.Renderer as IRendererWithCompositor)?.Compositor; + RequestNextFrameRendering(); + } - if (_context == null) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create additional OpenGL context."); - return false; - } + private bool EnsureInitializedCore( + ICompositionGpuInterop interop, + IOpenGlTextureSharingRenderInterfaceContextFeature contextSharingFeature) + { + var surface = _compositor.CreateDrawingSurface(); - GlVersion = _context.Version; + IGlContext ctx = null; + var contextFactory = AvaloniaLocator.Current.GetService(); try { - _bitmap = new OpenGlBitmap(_feature, GetPixelSize(), new Vector(96, 96)); - if (!_bitmap.SupportsContext(_context)) + if (contextSharingFeature?.CanCreateSharedContext == true) + _resources = OpenGlControlBaseResources.TryCreate(surface, interop, contextSharingFeature); + + if(_resources == null) + { + ctx = contextFactory.CreateContext(null); + if (ctx.TryGetFeature(out var externalObjects)) + _resources = OpenGlControlBaseResources.TryCreate(ctx, surface, interop, externalObjects); + else + ctx.Dispose(); + } + + if(_resources == null) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create OpenGlBitmap: OpenGL context is not compatible"); + "Unable to initialize OpenGL: current platform does not support multithreaded context sharing and shared memory"); return false; } } catch (Exception e) { - _context.Dispose(); - _context = null; Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: unable to create OpenGlBitmap: {exception}", e); + "Unable to initialize OpenGL: {exception}", e); + ctx?.Dispose(); return false; } + + _visual = _compositor.CreateSurfaceVisual(); + _visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + _visual.Surface = _resources.Surface; + ElementComposition.SetElementChildVisual(this, _visual); + using (_resources.Context.MakeCurrent()) + OnOpenGlInit(_resources.Context.GlInterface); + return true; - using (_context.MakeCurrent()) - { - try - { - _depthBufferSize = GetPixelSize(); - var gl = _context.GlInterface; - _fb = gl.GenFramebuffer(); - gl.BindFramebuffer(GL_FRAMEBUFFER, _fb); - - EnsureDepthBufferAttachment(gl); - EnsureTextureAttachment(); - - return CheckFramebufferStatus(gl); - } - catch(Exception e) - { - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL FBO: {exception}", e); - return false; - } - } } - - private static bool CheckFramebufferStatus(GlInterface gl) + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); - if (status != GL_FRAMEBUFFER_COMPLETE) + if (_visual != null && change.Property == BoundsProperty) { - int code; - while ((code = gl.GetError()) != 0) - Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL FBO: {code}", code); - return false; + _visual.Size = new Vector2((float)Bounds.Width, (float)Bounds.Height); + RequestNextFrameRendering(); } - return true; + base.OnPropertyChanged(change); } void ContextLost() { - _context = null; - _feature = null; _initialization = null; - _attachment = null; - _bitmap = null; - _fb = 0; - _depthBuffer = 0; - _depthBufferSize = default; + _resources?.DisposeAsync(); OnOpenGlLost(); } @@ -228,59 +141,106 @@ namespace Avalonia.OpenGL.Controls if (_initialization is { IsCompleted: false }) return false; - if (_context.IsLost) + if (_resources!.Context.IsLost) ContextLost(); else return true; } _initialization = InitializeAsync(); + + async void ContinueOnInitialization() + { + try + { + await _initialization; + RequestNextFrameRendering(); + } + catch + { + // + } + } + ContinueOnInitialization(); return false; } + + private void Update() + { + _updateQueued = false; + if (VisualRoot == null) + return; + if(!EnsureInitialized()) + return; + using (_resources.BeginDraw(GetPixelSize())) + OnOpenGlRender(_resources.Context.GlInterface, _resources.Fbo); + } + private async Task InitializeAsync() { + if (_compositor == null) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to obtain Compositor instance"); + return false; + } + + var gpuInteropTask = _compositor.TryGetCompositionGpuInterop(); + var contextSharingFeature = (IOpenGlTextureSharingRenderInterfaceContextFeature) - await this.GetVisualRoot()!.Renderer.TryGetRenderInterfaceFeature( + await _compositor.TryGetRenderInterfaceFeature( typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)); + var interop = await gpuInteropTask; - if (contextSharingFeature == null || !contextSharingFeature.CanCreateSharedContext) + if (interop == null) { Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", - "Unable to initialize OpenGL: current platform does not support multithreaded context sharing"); + "Compositor backend doesn't support GPU interop"); return false; } - if (!EnsureInitializedCore(contextSharingFeature)) + if (!EnsureInitializedCore(interop, contextSharingFeature)) { DoCleanup(); return false; } - using (_context.MakeCurrent()) - OnOpenGlInit(_context.GlInterface, _fb); - - InvalidateVisual(); + using (_resources!.Context.MakeCurrent()) + OnOpenGlInit(_resources.Context.GlInterface); return true; } + + [Obsolete("Use RequestNextFrameRendering()")] + // ReSharper disable once MemberCanBeProtected.Global + public new void InvalidateVisual() => RequestNextFrameRendering(); + public void RequestNextFrameRendering() + { + if ((_initialization == null || _initialization is { Status: TaskStatus.RanToCompletion }) && + !_updateQueued) + { + _updateQueued = true; + _compositor?.RequestCompositionUpdate(_update); + } + } + private PixelSize GetPixelSize() { - var scaling = VisualRoot.RenderScaling; + var scaling = VisualRoot!.RenderScaling; return new PixelSize(Math.Max(1, (int)(Bounds.Width * scaling)), Math.Max(1, (int)(Bounds.Height * scaling))); } - - - protected virtual void OnOpenGlInit(GlInterface gl, int fb) + + protected virtual void OnOpenGlInit(GlInterface gl) { } - protected virtual void OnOpenGlDeinit(GlInterface gl, int fb) + protected virtual void OnOpenGlDeinit(GlInterface gl) { } diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs new file mode 100644 index 0000000000..81270bb642 --- /dev/null +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs @@ -0,0 +1,170 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Avalonia.Logging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using static Avalonia.OpenGL.GlConsts; +namespace Avalonia.OpenGL.Controls; + +internal class OpenGlControlBaseResources : IAsyncDisposable +{ + private int _depthBuffer; + public int Fbo { get; private set; } + private PixelSize _depthBufferSize; + public CompositionDrawingSurface Surface { get; } + public CompositionOpenGlSwapchain _swapchain; + public IGlContext Context { get; private set; } + + public static OpenGlControlBaseResources? TryCreate(CompositionDrawingSurface surface, + ICompositionGpuInterop interop, + IOpenGlTextureSharingRenderInterfaceContextFeature feature) + { + IGlContext context; + try + { + context = feature.CreateSharedContext(); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create additional OpenGL context: {exception}", e); + return null; + } + + if (context == null) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to initialize OpenGL: unable to create additional OpenGL context."); + return null; + } + + return new OpenGlControlBaseResources(context, surface, interop, feature, null); + } + + public static OpenGlControlBaseResources? TryCreate(IGlContext context, CompositionDrawingSurface surface, + ICompositionGpuInterop interop, IGlContextExternalObjectsFeature externalObjects) + { + if (!interop.SupportedImageHandleTypes.Contains(KnownPlatformGraphicsExternalImageHandleTypes + .D3D11TextureGlobalSharedHandle) + || !externalObjects.SupportedExportableExternalImageTypes.Contains( + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle)) + return null; + return new OpenGlControlBaseResources(context, surface, interop, null, externalObjects); + } + + public OpenGlControlBaseResources(IGlContext context, + CompositionDrawingSurface surface, + ICompositionGpuInterop interop, + IOpenGlTextureSharingRenderInterfaceContextFeature? feature, + IGlContextExternalObjectsFeature? externalObjects + ) + { + Context = context; + Surface = surface; + using (context.MakeCurrent()) + Fbo = context.GlInterface.GenFramebuffer(); + _swapchain = + feature != null ? + new CompositionOpenGlSwapchain(context, interop, Surface, feature) : + new CompositionOpenGlSwapchain(context, interop, Surface, externalObjects); + } + + void UpdateDepthRenderbuffer(PixelSize size) + { + if (size == _depthBufferSize && _depthBuffer != 0) + return; + + var gl = Context.GlInterface; + gl.GetIntegerv(GL_RENDERBUFFER_BINDING, out var oldRenderBuffer); + if (_depthBuffer != 0) gl.DeleteRenderbuffer(_depthBuffer); + + _depthBuffer = gl.GenRenderbuffer(); + gl.BindRenderbuffer(GL_RENDERBUFFER, _depthBuffer); + gl.RenderbufferStorage(GL_RENDERBUFFER, + Context.Version.Type == GlProfileType.OpenGLES ? GL_DEPTH_COMPONENT16 : GL_DEPTH_COMPONENT, + size.Width, size.Height); + gl.FramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, _depthBuffer); + gl.BindRenderbuffer(GL_RENDERBUFFER, oldRenderBuffer); + _depthBufferSize = size; + } + + public IDisposable BeginDraw(PixelSize size) + { + var restoreContext = Context.EnsureCurrent(); + IDisposable? imagePresent = null; + var success = false; + try + { + var gl = Context.GlInterface; + Context.GlInterface.BindFramebuffer(GL_FRAMEBUFFER, Fbo); + UpdateDepthRenderbuffer(size); + + imagePresent = _swapchain.BeginDraw(size, out var texture); + gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture.TextureId, 0); + + var status = gl.CheckFramebufferStatus(GL_FRAMEBUFFER); + if (status != GL_FRAMEBUFFER_COMPLETE) + { + int code = gl.GetError(); + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log("OpenGlControlBase", + "Unable to configure OpenGL FBO: {code}", code); + throw OpenGlException.GetFormattedException("Unable to configure OpenGL FBO", code); + } + + success = true; + return Disposable.Create(() => + { + try + { + Context.GlInterface.Flush(); + imagePresent.Dispose(); + } + finally + { + restoreContext.Dispose(); + } + }); + } + finally + { + if (!success) + { + imagePresent?.Dispose(); + restoreContext.Dispose(); + } + } + } + + public async ValueTask DisposeAsync() + { + if (Context is { IsLost: false }) + { + try + { + using (Context.MakeCurrent()) + { + var gl = Context.GlInterface; + if (Fbo != 0) + gl.DeleteFramebuffer(Fbo); + Fbo = 0; + if (_depthBuffer != 0) + gl.DeleteRenderbuffer(_depthBuffer); + _depthBuffer = 0; + } + + } + catch + { + // + } + + Surface.Dispose(); + await _swapchain.DisposeAsync(); + + Context = null!; + } + } +} diff --git a/src/Avalonia.OpenGL/Egl/EglConsts.cs b/src/Avalonia.OpenGL/Egl/EglConsts.cs index 428d11857f..aff56ecfe0 100644 --- a/src/Avalonia.OpenGL/Egl/EglConsts.cs +++ b/src/Avalonia.OpenGL/Egl/EglConsts.cs @@ -64,7 +64,7 @@ namespace Avalonia.OpenGL.Egl public const int EGL_WIDTH = 0x3057; public const int EGL_WINDOW_BIT = 0x0004; -// public const int EGL_BACK_BUFFER = 0x3084; + public const int EGL_BACK_BUFFER = 0x3084; // public const int EGL_BIND_TO_TEXTURE_RGB = 0x3039; // public const int EGL_BIND_TO_TEXTURE_RGBA = 0x303A; public const int EGL_CONTEXT_LOST = 0x300E; @@ -73,11 +73,11 @@ namespace Avalonia.OpenGL.Egl // public const int EGL_MIPMAP_TEXTURE = 0x3082; // public const int EGL_MIPMAP_LEVEL = 0x3083; // public const int EGL_NO_TEXTURE = 0x305C; -// public const int EGL_TEXTURE_2D = 0x305F; -// public const int EGL_TEXTURE_FORMAT = 0x3080; + public const int EGL_TEXTURE_2D = 0x305F; + public const int EGL_TEXTURE_FORMAT = 0x3080; // public const int EGL_TEXTURE_RGB = 0x305D; -// public const int EGL_TEXTURE_RGBA = 0x305E; -// public const int EGL_TEXTURE_TARGET = 0x3081; + public const int EGL_TEXTURE_RGBA = 0x305E; + public const int EGL_TEXTURE_TARGET = 0x3081; // public const int EGL_ALPHA_FORMAT = 0x3088; // public const int EGL_ALPHA_FORMAT_NONPRE = 0x308B; @@ -216,5 +216,7 @@ namespace Avalonia.OpenGL.Egl public const int EGL_TEXTURE_OFFSET_Y_ANGLE = 0x3491; public const int EGL_FLEXIBLE_SURFACE_COMPATIBILITY_SUPPORTED_ANGLE = 0x33A6; + + public const int EGL_TEXTURE_INTERNAL_FORMAT_ANGLE = 0x345D; } } diff --git a/src/Avalonia.OpenGL/Egl/EglContext.cs b/src/Avalonia.OpenGL/Egl/EglContext.cs index 4d75a776c3..425206e3c4 100644 --- a/src/Avalonia.OpenGL/Egl/EglContext.cs +++ b/src/Avalonia.OpenGL/Egl/EglContext.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Disposables; using System.Threading; using Avalonia.Platform; @@ -19,21 +20,24 @@ namespace Avalonia.OpenGL.Egl private readonly object _lock; internal EglContext(EglDisplay display, EglInterface egl, EglContext sharedWith, IntPtr ctx, EglSurface offscreenSurface, - GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, Dictionary features) + GlVersion version, int sampleCount, int stencilSize, Action disposeCallback, + Dictionary> features) { _disp = display; _egl = egl; _sharedWith = sharedWith; _context = ctx; _disposeCallback = disposeCallback; - _features = features; OffscreenSurface = offscreenSurface; Version = version; SampleCount = sampleCount; StencilSize = stencilSize; _lock = display.ContextSharedSyncRoot ?? new object(); using (MakeCurrent()) + { GlInterface = GlInterface.FromNativeUtf8GetProcAddress(version, _egl.GetProcAddress); + _features = features.ToDictionary(x => x.Key, x => x.Value(this)); + } } public IntPtr Context => @@ -155,6 +159,14 @@ namespace Avalonia.OpenGL.Egl { if(_context == IntPtr.Zero) return; + + foreach(var f in _features.ToList()) + if (f.Value is IDisposable d) + { + d.Dispose(); + _features.Remove(f.Key); + } + _egl.DestroyContext(_disp.Handle, Context); OffscreenSurface?.Dispose(); _context = IntPtr.Zero; diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index eea2587587..e4f0b2826d 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -93,7 +93,7 @@ namespace Avalonia.OpenGL.Egl var rv = new EglContext(this, _egl, share, ctx, offscreenSurface, _config.Version, _config.SampleCount, _config.StencilSize, - options.DisposeCallback, options.ExtraFeatures); + options.DisposeCallback, options.ExtraFeatures ?? new()); _contexts.Add(rv); return rv; } diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs index 5648645c54..906a533e2d 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs @@ -19,7 +19,7 @@ public class EglContextOptions public EglContext ShareWith { get; set; } public EglSurface OffscreenSurface { get; set; } public Action DisposeCallback { get; set; } - public Dictionary ExtraFeatures { get; set; } + public Dictionary> ExtraFeatures { get; set; } } public class EglDisplayCreationOptions : EglDisplayOptions diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index a913c05996..67fd172e8e 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -98,6 +98,9 @@ namespace Avalonia.OpenGL.Egl [GetProcAddress("eglCreateWindowSurface")] public partial IntPtr CreateWindowSurface(IntPtr display, IntPtr config, IntPtr window, int[] attrs); + [GetProcAddress("eglBindTexImage")] + public partial int BindTexImage(IntPtr display, IntPtr surface, int buffer); + [GetProcAddress("eglGetConfigAttrib")] public partial bool GetConfigAttrib(IntPtr display, IntPtr config, int attr, out int rv); diff --git a/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs b/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs new file mode 100644 index 0000000000..02d152ff61 --- /dev/null +++ b/src/Avalonia.OpenGL/Features/ExternalObjectsOpenGlExtensionFeature.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using Avalonia.Logging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Avalonia.SourceGenerator; +using static Avalonia.OpenGL.GlConsts; + +namespace Avalonia.OpenGL.Features; + +unsafe partial class ExternalObjectsInterface +{ + public ExternalObjectsInterface(Func getProcAddress) + { + Initialize(getProcAddress); + } + + [GetProcAddress("glImportMemoryFdEXT", true)] + public partial void ImportMemoryFdEXT(uint memory, ulong size, int handleType, int fd); + + [GetProcAddress("glImportSemaphoreFdEXT", true)] + public partial void ImportSemaphoreFdEXT(uint semaphore, + int handleType, + int fd); + + [GetProcAddress("glCreateMemoryObjectsEXT")] + public partial void CreateMemoryObjectsEXT(int n, out uint memoryObjects); + + [GetProcAddress("glDeleteMemoryObjectsEXT")] + public partial void DeleteMemoryObjectsEXT(int n, ref uint objects); + + [GetProcAddress("glTexStorageMem2DEXT")] + public partial void TexStorageMem2DEXT(int target, int levels, int internalFormat, int width, int height, + uint memory, ulong offset); + + [GetProcAddress("glGenSemaphoresEXT")] + public partial void GenSemaphoresEXT(int n, out uint semaphores); + + [GetProcAddress("glDeleteSemaphoresEXT")] + public partial void DeleteSemaphoresEXT(int n, ref uint semaphores); + + [GetProcAddress("glWaitSemaphoreEXT")] + public partial void WaitSemaphoreEXT(uint semaphore, + uint numBufferBarriers, uint* buffers, + uint numTextureBarriers, int* textures, + int* srcLayouts); + + [GetProcAddress("glSignalSemaphoreEXT")] + public partial void SignalSemaphoreEXT(uint semaphore, + uint numBufferBarriers, uint* buffers, + uint numTextureBarriers, int* textures, + int* dstLayouts); + + + [GetProcAddress("glGetUnsignedBytei_vEXT", true)] + public partial void GetUnsignedBytei_vEXT(int target, uint index, byte* data); + + [GetProcAddress("glGetUnsignedBytevEXT", true)] + public partial void GetUnsignedBytevEXT(int target, byte* data); +} + +public class ExternalObjectsOpenGlExtensionFeature : IGlContextExternalObjectsFeature +{ + private readonly IGlContext _context; + private readonly ExternalObjectsInterface _ext; + private List _imageTypes = new(); + private List _semaphoreTypes = new(); + + public static ExternalObjectsOpenGlExtensionFeature TryCreate(IGlContext context) + { + var extensions = context.GlInterface.GetExtensions(); + if (extensions.Contains("GL_EXT_memory_object") && extensions.Contains("GL_EXT_semaphore")) + { + try + { + return new ExternalObjectsOpenGlExtensionFeature(context, extensions); + } + catch (Exception e) + { + Logger.TryGet(LogEventLevel.Error, "OpenGL")?.Log(nameof(ExternalObjectsOpenGlExtensionFeature), + "Unable to initialize EXT_external_objects extension: " + e); + } + } + + return null; + } + + private unsafe ExternalObjectsOpenGlExtensionFeature(IGlContext context, List extensions) + { + _context = context; + _ext = new ExternalObjectsInterface(_context.GlInterface.GetProcAddress); + + if (_ext.IsGetUnsignedBytei_vEXTAvailable) + { + _context.GlInterface.GetIntegerv(GL_NUM_DEVICE_UUIDS_EXT, out var numUiids); + if (numUiids > 0) + { + DeviceUuid = new byte[16]; + fixed (byte* pUuid = DeviceUuid) + _ext.GetUnsignedBytei_vEXT(GL_DEVICE_UUID_EXT, 0, pUuid); + } + } + + if (_ext.IsGetUnsignedBytevEXTAvailable) + { + if (extensions.Contains("GL_EXT_memory_object_win32") || extensions.Contains("GL_EXT_semaphore_win32")) + { + DeviceLuid = new byte[8]; + fixed (byte* pLuid = DeviceLuid) + _ext.GetUnsignedBytevEXT(GL_DEVICE_LUID_EXT, pLuid); + } + } + + if (extensions.Contains("GL_EXT_memory_object_fd") + && extensions.Contains("GL_EXT_semaphore_fd")) + { + _imageTypes.Add(KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor); + _semaphoreTypes.Add(KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor); + } + + + + } + + public IReadOnlyList SupportedImportableExternalImageTypes => _imageTypes; + public IReadOnlyList SupportedExportableExternalImageTypes { get; } = Array.Empty(); + public IReadOnlyList SupportedImportableExternalSemaphoreTypes => _semaphoreTypes; + public IReadOnlyList SupportedExportableExternalSemaphoreTypes { get; } = Array.Empty(); + public IReadOnlyList GetSupportedFormatsForExternalMemoryType(string type) + { + return new[] + { + PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm + }; + } + + public IGlExportableExternalImageTexture CreateImage(string type, PixelSize size, + PlatformGraphicsExternalImageFormat format) => + throw new NotSupportedException(); + + public IGlExportableExternalImageTexture CreateSemaphore(string type) => throw new NotSupportedException(); + + public IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) + { + if(!_imageTypes.Contains(handle.HandleDescriptor)) + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + + if (handle.HandleDescriptor == KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor) + { + while (_context.GlInterface.GetError() != 0) + { + //Skip existing errors + } + _ext.CreateMemoryObjectsEXT(1, out var memoryObject); + _ext.ImportMemoryFdEXT(memoryObject, properties.MemorySize, GL_HANDLE_TYPE_OPAQUE_FD_EXT, + handle.Handle.ToInt32()); + + var err = _context.GlInterface.GetError(); + if (err != 0) + throw OpenGlException.GetFormattedException("glImportMemoryFdEXT", err); + + _context.GlInterface.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); + + var texture = _context.GlInterface.GenTexture(); + _context.GlInterface.BindTexture(GL_TEXTURE_2D, texture); + _ext.TexStorageMem2DEXT(GL_TEXTURE_2D, 1, GL_RGBA8, properties.Width, properties.Height, + memoryObject, properties.MemoryOffset); + err = _context.GlInterface.GetError(); + + _context.GlInterface.BindTexture(GL_TEXTURE_2D, oldTexture); + if (err != 0) + throw OpenGlException.GetFormattedException("glTexStorageMem2DEXT", err); + + return new ExternalImageTexture(_context, properties, _ext, memoryObject, texture); + } + + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + } + + public IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle) + { + if(!_semaphoreTypes.Contains(handle.HandleDescriptor)) + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + + if (handle.HandleDescriptor == + KnownPlatformGraphicsExternalSemaphoreHandleTypes.VulkanOpaquePosixFileDescriptor) + { + _ext.GenSemaphoresEXT(1, out var semaphore); + _ext.ImportSemaphoreFdEXT(semaphore, GL_HANDLE_TYPE_OPAQUE_FD_EXT, handle.Handle.ToInt32()); + return new ExternalSemaphore(_context, _ext, semaphore); + } + + throw new ArgumentException(handle.HandleDescriptor + " is not supported"); + } + + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + { + if (imageHandleType == KnownPlatformGraphicsExternalImageHandleTypes.VulkanOpaquePosixFileDescriptor) + return CompositionGpuImportedImageSynchronizationCapabilities.Semaphores; + return default; + } + + public byte[] DeviceLuid { get; } + public byte[] DeviceUuid { get; } + + unsafe class ExternalSemaphore : IGlExternalSemaphore + { + private readonly IGlContext _context; + private readonly ExternalObjectsInterface _ext; + private uint _semaphore; + + public ExternalSemaphore(IGlContext context, ExternalObjectsInterface ext, uint semaphore) + { + _context = context; + _ext = ext; + _semaphore = semaphore; + } + + public void Dispose() + { + if(_context.IsLost) + return; + using (_context.EnsureCurrent()) + _ext.DeleteSemaphoresEXT(1, ref _semaphore); + _semaphore = 0; + } + + public void WaitSemaphore(IGlExternalImageTexture texture) + { + var tex = (ExternalImageTexture)texture; + var texId = tex.TextureId; + var srcLayout = GL_LAYOUT_TRANSFER_SRC_EXT; + _ext.WaitSemaphoreEXT(_semaphore, 0, null, 1, &texId, &srcLayout); + } + + public void SignalSemaphore(IGlExternalImageTexture texture) + { + var tex = (ExternalImageTexture)texture; + var texId = tex.TextureId; + var dstLayout = 0; + _ext.SignalSemaphoreEXT(_semaphore, 0, null, 1, &texId, &dstLayout); + } + } + + class ExternalImageTexture : IGlExternalImageTexture + { + private readonly IGlContext _context; + private readonly ExternalObjectsInterface _ext; + private uint _objectId; + + public ExternalImageTexture(IGlContext context, + PlatformGraphicsExternalImageProperties properties, + ExternalObjectsInterface ext, uint objectId, int textureId) + { + Properties = properties; + TextureId = textureId; + _context = context; + _ext = ext; + _objectId = objectId; + } + + public void Dispose() + { + if(_context.IsLost) + return; + using (_context.EnsureCurrent()) + { + _context.GlInterface.DeleteTexture(TextureId); + _ext.DeleteMemoryObjectsEXT(1, ref _objectId); + _objectId = 0; + } + } + + public void AcquireKeyedMutex(uint key) => throw new NotSupportedException(); + + public void ReleaseKeyedMutex(uint key) => throw new NotSupportedException(); + + public int TextureId { get; } + public int InternalFormat => GL_RGBA8; + public PlatformGraphicsExternalImageProperties Properties { get; } + } +} diff --git a/src/Avalonia.OpenGL/GlConsts.cs b/src/Avalonia.OpenGL/GlConsts.cs index 3fdc147701..4fbe738ca9 100644 --- a/src/Avalonia.OpenGL/GlConsts.cs +++ b/src/Avalonia.OpenGL/GlConsts.cs @@ -534,7 +534,7 @@ namespace Avalonia.OpenGL // public const int GL_MAX_ELEMENTS_VERTICES = 0x80E8; // public const int GL_MAX_ELEMENTS_INDICES = 0x80E9; // public const int GL_BGR = 0x80E0; -// public const int GL_BGRA = 0x80E1; + public const int GL_BGRA = 0x80E1; // public const int GL_UNSIGNED_BYTE_3_3_2 = 0x8032; // public const int GL_UNSIGNED_BYTE_2_3_3_REV = 0x8362; // public const int GL_UNSIGNED_SHORT_5_6_5 = 0x8363; @@ -3372,16 +3372,16 @@ namespace Avalonia.OpenGL // public const int GL_TILING_TYPES_EXT = 0x9583; // public const int GL_OPTIMAL_TILING_EXT = 0x9584; // public const int GL_LINEAR_TILING_EXT = 0x9585; -// public const int GL_NUM_DEVICE_UUIDS_EXT = 0x9596; -// public const int GL_DEVICE_UUID_EXT = 0x9597; + internal const int GL_NUM_DEVICE_UUIDS_EXT = 0x9596; + internal const int GL_DEVICE_UUID_EXT = 0x9597; // public const int GL_DRIVER_UUID_EXT = 0x9598; // public const int GL_UUID_SIZE_EXT = 16; // public const int GL_EXT_memory_object_fd = 1; -// public const int GL_HANDLE_TYPE_OPAQUE_FD_EXT = 0x9586; + internal const int GL_HANDLE_TYPE_OPAQUE_FD_EXT = 0x9586; // public const int GL_EXT_memory_object_win32 = 1; // public const int GL_HANDLE_TYPE_OPAQUE_WIN32_EXT = 0x9587; // public const int GL_HANDLE_TYPE_OPAQUE_WIN32_KMT_EXT = 0x9588; -// public const int GL_DEVICE_LUID_EXT = 0x9599; + public const int GL_DEVICE_LUID_EXT = 0x9599; // public const int GL_DEVICE_NODE_MASK_EXT = 0x959A; // public const int GL_LUID_SIZE_EXT = 8; // public const int GL_HANDLE_TYPE_D3D12_TILEPOOL_EXT = 0x9589; @@ -3483,11 +3483,11 @@ namespace Avalonia.OpenGL // public const int GL_SECONDARY_COLOR_ARRAY_EXT = 0x845E; // public const int GL_EXT_semaphore = 1; // public const int GL_LAYOUT_GENERAL_EXT = 0x958D; -// public const int GL_LAYOUT_COLOR_ATTACHMENT_EXT = 0x958E; + internal const int GL_LAYOUT_COLOR_ATTACHMENT_EXT = 0x958E; // public const int GL_LAYOUT_DEPTH_STENCIL_ATTACHMENT_EXT = 0x958F; // public const int GL_LAYOUT_DEPTH_STENCIL_READ_ONLY_EXT = 0x9590; // public const int GL_LAYOUT_SHADER_READ_ONLY_EXT = 0x9591; -// public const int GL_LAYOUT_TRANSFER_SRC_EXT = 0x9592; + internal const int GL_LAYOUT_TRANSFER_SRC_EXT = 0x9592; // public const int GL_LAYOUT_TRANSFER_DST_EXT = 0x9593; // public const int GL_LAYOUT_DEPTH_READ_ONLY_STENCIL_ATTACHMENT_EXT = 0x9530; // public const int GL_LAYOUT_DEPTH_ATTACHMENT_STENCIL_READ_ONLY_EXT = 0x9531; diff --git a/src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs b/src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs new file mode 100644 index 0000000000..676f8eaf86 --- /dev/null +++ b/src/Avalonia.OpenGL/IGlContextExternalObjectsFeature.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; + +namespace Avalonia.OpenGL; + +public interface IGlContextExternalObjectsFeature +{ + IReadOnlyList SupportedImportableExternalImageTypes { get; } + IReadOnlyList SupportedExportableExternalImageTypes { get; } + IReadOnlyList SupportedImportableExternalSemaphoreTypes { get; } + IReadOnlyList SupportedExportableExternalSemaphoreTypes { get; } + IReadOnlyList GetSupportedFormatsForExternalMemoryType(string type); + + IGlExportableExternalImageTexture CreateImage(string type,PixelSize size, PlatformGraphicsExternalImageFormat format); + + IGlExportableExternalImageTexture CreateSemaphore(string type); + IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties); + IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle); + CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType); + public byte[]? DeviceLuid { get; } + public byte[]? DeviceUuid { get; } +} + +public interface IGlExternalSemaphore : IDisposable +{ + void WaitSemaphore(IGlExternalImageTexture texture); + void SignalSemaphore(IGlExternalImageTexture texture); +} + +public interface IGlExportableExternalSemaphore : IGlExternalSemaphore +{ + IPlatformHandle GetHandle(); +} + +public interface IGlExternalImageTexture : IDisposable +{ + void AcquireKeyedMutex(uint key); + void ReleaseKeyedMutex(uint key); + int TextureId { get; } + int InternalFormat { get; } + + PlatformGraphicsExternalImageProperties Properties { get; } +} + +public interface IGlExportableExternalImageTexture : IGlExternalImageTexture +{ + IPlatformHandle GetHandle(); +} diff --git a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs index 9c22d446ef..2043e944d1 100644 --- a/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs +++ b/src/Avalonia.OpenGL/IOpenGlTextureSharingRenderInterfaceContextFeature.cs @@ -1,6 +1,6 @@ +using System; using System.Collections.Generic; -using Avalonia.OpenGL.Imaging; -using Avalonia.Platform; +using Avalonia.Rendering.Composition; namespace Avalonia.OpenGL { @@ -8,6 +8,13 @@ namespace Avalonia.OpenGL { bool CanCreateSharedContext { get; } IGlContext CreateSharedContext(IEnumerable preferredVersions = null); - IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); + ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size); + } + + public interface ICompositionImportableOpenGlSharedTexture : ICompositionImportableSharedGpuContextImage + { + int TextureId { get; } + int InternalFormat { get; } + PixelSize Size { get; } } } diff --git a/src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs b/src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs new file mode 100644 index 0000000000..e6c83415e6 --- /dev/null +++ b/src/Avalonia.OpenGL/IPlatformGraphicsOpenGlContextFactory.cs @@ -0,0 +1,8 @@ +using System.Collections.Generic; + +namespace Avalonia.OpenGL; + +public interface IPlatformGraphicsOpenGlContextFactory +{ + IGlContext CreateContext(IEnumerable? versions); +} diff --git a/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs b/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs deleted file mode 100644 index 22f0cebf57..0000000000 --- a/src/Avalonia.OpenGL/Imaging/IOpenGlBitmapImpl.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using Avalonia.Metadata; -using Avalonia.Platform; - -namespace Avalonia.OpenGL.Imaging -{ - [Unstable] - public interface IOpenGlBitmapImpl : IBitmapImpl - { - IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback); - bool SupportsContext(IGlContext context); - } - - [Unstable] - public interface IOpenGlBitmapAttachment : IDisposable - { - void Present(); - } -} diff --git a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs b/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs deleted file mode 100644 index 53013ae5a3..0000000000 --- a/src/Avalonia.OpenGL/Imaging/OpenGlBitmap.cs +++ /dev/null @@ -1,40 +0,0 @@ -using System; -using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Threading; - -namespace Avalonia.OpenGL.Imaging -{ - public class OpenGlBitmap : Bitmap, IAffectsRender - { - private IOpenGlBitmapImpl _impl; - - public OpenGlBitmap(IOpenGlTextureSharingRenderInterfaceContextFeature feature, - PixelSize size, Vector dpi) - : base(CreateOrThrow(feature, size, dpi)) - { - _impl = (IOpenGlBitmapImpl)PlatformImpl.Item; - } - - static IOpenGlBitmapImpl CreateOrThrow(IOpenGlTextureSharingRenderInterfaceContextFeature feature, - PixelSize size, Vector dpi) => feature.CreateOpenGlBitmap(size, dpi); - - public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context) => - _impl.CreateFramebufferAttachment(context, SetIsDirty); - - public bool SupportsContext(IGlContext context) => _impl.SupportsContext(context); - - void SetIsDirty() - { - if (Dispatcher.UIThread.CheckAccess()) - CallInvalidated(); - else - Dispatcher.UIThread.Post(CallInvalidated); - } - - private void CallInvalidated() => Invalidated?.Invoke(this, EventArgs.Empty); - - public event EventHandler Invalidated; - } -} diff --git a/src/Avalonia.OpenGL/OpenGlException.cs b/src/Avalonia.OpenGL/OpenGlException.cs index efe305dba5..c498ed7833 100644 --- a/src/Avalonia.OpenGL/OpenGlException.cs +++ b/src/Avalonia.OpenGL/OpenGlException.cs @@ -27,6 +27,9 @@ namespace Avalonia.OpenGL return GetFormattedException(funcName, (GlErrors)err, err); } + public static OpenGlException GetFormattedException(string funcName, int errorCode) => + GetFormattedException(funcName, (GlErrors)errorCode, errorCode); + public static OpenGlException GetFormattedEglException(string funcName, int errorCode) => GetFormattedException(funcName, (EglErrors)errorCode,errorCode); diff --git a/src/Avalonia.X11/Glx/GlxContext.cs b/src/Avalonia.X11/Glx/GlxContext.cs index def5228e94..dd10beb32e 100644 --- a/src/Avalonia.X11/Glx/GlxContext.cs +++ b/src/Avalonia.X11/Glx/GlxContext.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Reactive.Disposables; using System.Threading; using Avalonia.OpenGL; +using Avalonia.OpenGL.Features; + namespace Avalonia.X11.Glx { class GlxContext : IGlContext @@ -14,6 +16,7 @@ namespace Avalonia.X11.Glx private readonly IntPtr _defaultXid; private readonly bool _ownsPBuffer; private readonly object _lock = new object(); + private ExternalObjectsOpenGlExtensionFeature? _externalObjects; public GlxContext(GlxInterface glx, IntPtr handle, GlxDisplay display, GlxContext sharedWith, @@ -32,7 +35,10 @@ namespace Avalonia.X11.Glx SampleCount = sampleCount; StencilSize = stencilSize; using (MakeCurrent()) + { GlInterface = new GlInterface(version, GlxInterface.SafeGetProcAddress); + _externalObjects = ExternalObjectsOpenGlExtensionFeature.TryCreate(this); + } } public GlxDisplay Display { get; } @@ -123,6 +129,11 @@ namespace Avalonia.X11.Glx Glx.DestroyPbuffer(_x11.Display, _defaultXid); } - public object TryGetFeature(Type featureType) => null; + public object TryGetFeature(Type featureType) + { + if (featureType == typeof(IGlContextExternalObjectsFeature)) + return _externalObjects; + return null; + } } } diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index b064445a0b..a5782037f3 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.Platform; using SkiaSharp; @@ -33,9 +31,4 @@ namespace Avalonia.Skia bool CanBlit { get; } void Blit(SKCanvas canvas); } - - public interface IOpenGlAwareSkiaGpu : ISkiaGpu - { - IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi); - } } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs new file mode 100644 index 0000000000..4bf43634ef --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaExternalObjectsFeature.cs @@ -0,0 +1,191 @@ +#nullable enable +using System; +using System.Collections.Generic; +using Avalonia.OpenGL; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using SkiaSharp; + +namespace Avalonia.Skia; + +internal class GlSkiaExternalObjectsFeature : IExternalObjectsRenderInterfaceContextFeature +{ + private readonly GlSkiaGpu _gpu; + private readonly IGlContextExternalObjectsFeature? _feature; + + public GlSkiaExternalObjectsFeature(GlSkiaGpu gpu, IGlContextExternalObjectsFeature? feature) + { + _gpu = gpu; + _feature = feature; + } + + public IReadOnlyList SupportedImageHandleTypes => _feature?.SupportedImportableExternalImageTypes + ?? Array.Empty(); + public IReadOnlyList SupportedSemaphoreTypes => _feature?.SupportedImportableExternalSemaphoreTypes + ?? Array.Empty(); + + public IPlatformRenderInterfaceImportedImage ImportImage(IPlatformHandle handle, + PlatformGraphicsExternalImageProperties properties) + { + if (_feature == null) + throw new NotSupportedException("Importing this platform handle is not supported"); + using (_gpu.EnsureCurrent()) + { + var image = _feature.ImportImage(handle, properties); + return new GlSkiaImportedImage(_gpu, image); + } + } + + public IPlatformRenderInterfaceImportedImage ImportImage(ICompositionImportableSharedGpuContextImage image) + { + var img = (GlSkiaSharedTextureForComposition)image; + if (!img.Context.IsSharedWith(_gpu.GlContext)) + throw new InvalidOperationException("Contexts do not belong to the same share group"); + + return new GlSkiaImportedImage(_gpu, img); + } + + public IPlatformRenderInterfaceImportedSemaphore ImportSemaphore(IPlatformHandle handle) + { + if (_feature == null) + throw new NotSupportedException("Importing this platform handle is not supported"); + using (_gpu.EnsureCurrent()) + { + var semaphore = _feature.ImportSemaphore(handle); + return new GlSkiaImportedSemaphore(_gpu, semaphore); + } + } + + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + => _feature?.GetSynchronizationCapabilities(imageHandleType) ?? default; + + public byte[]? DeviceUuid => _feature?.DeviceUuid; + public byte[]? DeviceLuid => _feature?.DeviceLuid; +} + +internal class GlSkiaImportedSemaphore : IPlatformRenderInterfaceImportedSemaphore +{ + private readonly GlSkiaGpu _gpu; + public IGlExternalSemaphore Semaphore { get; } + + public GlSkiaImportedSemaphore(GlSkiaGpu gpu, IGlExternalSemaphore semaphore) + { + _gpu = gpu; + Semaphore = semaphore; + } + + public void Dispose() => Semaphore.Dispose(); +} + +internal class GlSkiaImportedImage : IPlatformRenderInterfaceImportedImage +{ + private readonly GlSkiaSharedTextureForComposition? _sharedTexture; + private readonly GlSkiaGpu _gpu; + private readonly IGlExternalImageTexture? _image; + + public GlSkiaImportedImage(GlSkiaGpu gpu, IGlExternalImageTexture image) + { + _gpu = gpu; + _image = image; + } + + public GlSkiaImportedImage(GlSkiaGpu gpu, GlSkiaSharedTextureForComposition sharedTexture) + { + _gpu = gpu; + _sharedTexture = sharedTexture; + } + + public void Dispose() + { + _image?.Dispose(); + _sharedTexture?.Dispose(_gpu.GlContext); + } + + SKColorType ConvertColorType(PlatformGraphicsExternalImageFormat format) => + format switch + { + PlatformGraphicsExternalImageFormat.B8G8R8A8UNorm => SKColorType.Bgra8888, + PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm => SKColorType.Rgba8888, + _ => SKColorType.Rgba8888 + }; + + SKSurface? TryCreateSurface(int textureId, int format, int width, int height, bool topLeft) + { + var origin = topLeft ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft; + using var texture = new GRBackendTexture(width, height, false, + new GRGlTextureInfo(GlConsts.GL_TEXTURE_2D, (uint)textureId, (uint)format)); + var surf = SKSurface.Create(_gpu.GrContext, texture, origin, SKColorType.Rgba8888); + if (surf != null) + return surf; + + using var unformatted = new GRBackendTexture(width, height, false, + new GRGlTextureInfo(GlConsts.GL_TEXTURE_2D, (uint)textureId)); + + return SKSurface.Create(_gpu.GrContext, unformatted, origin, SKColorType.Rgba8888); + } + + IBitmapImpl TakeSnapshot() + { + var width = _image?.Properties.Width ?? _sharedTexture!.Size.Width; + var height = _image?.Properties.Height ?? _sharedTexture!.Size.Height; + var internalFormat = _image?.InternalFormat ?? _sharedTexture!.InternalFormat; + var textureId = _image?.TextureId ?? _sharedTexture!.TextureId; + var topLeft = _image?.Properties.TopLeftOrigin ?? false; + + using var texture = new GRBackendTexture(width, height, false, + new GRGlTextureInfo(GlConsts.GL_TEXTURE_2D, (uint)textureId, (uint)internalFormat)); + + IBitmapImpl rv; + using (var surf = TryCreateSurface(textureId, internalFormat, width, height, topLeft)) + { + if (surf == null) + throw new OpenGlException("Unable to consume provided texture"); + rv = new ImmutableBitmap(surf.Snapshot()); + } + + _gpu.GrContext.Flush(); + _gpu.GlContext.GlInterface.Flush(); + return rv; + } + + public IBitmapImpl SnapshotWithKeyedMutex(uint acquireIndex, uint releaseIndex) + { + using (_gpu.EnsureCurrent()) + { + _image.AcquireKeyedMutex(acquireIndex); + try + { + return TakeSnapshot(); + } + finally + { + _image.ReleaseKeyedMutex(releaseIndex); + } + } + } + + public IBitmapImpl SnapshotWithSemaphores(IPlatformRenderInterfaceImportedSemaphore waitForSemaphore, + IPlatformRenderInterfaceImportedSemaphore signalSemaphore) + { + var wait = (GlSkiaImportedSemaphore)waitForSemaphore; + var signal = (GlSkiaImportedSemaphore)signalSemaphore; + using (_gpu.EnsureCurrent()) + { + wait.Semaphore.WaitSemaphore(_image); + try + { + return TakeSnapshot(); + } + finally + { + signal.Semaphore.SignalSemaphore(_image); + } + } + } + + public IBitmapImpl SnapshotWithAutomaticSync() + { + using (_gpu.EnsureCurrent()) + return TakeSnapshot(); + } +} diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index cdba3b9ea2..bf3e950e81 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -3,19 +3,22 @@ using System.Collections.Generic; using System.Runtime.InteropServices; using Avalonia.Logging; using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.OpenGL.Surfaces; using Avalonia.Platform; using SkiaSharp; +using static Avalonia.OpenGL.GlConsts; namespace Avalonia.Skia { - class GlSkiaGpu : IOpenGlAwareSkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature + class GlSkiaGpu : ISkiaGpu, IOpenGlTextureSharingRenderInterfaceContextFeature { private GRContext _grContext; private IGlContext _glContext; + public GRContext GrContext => _grContext; + public IGlContext GlContext => _glContext; private List _postDisposeCallbacks = new(); private bool? _canCreateSurfaces; + private IExternalObjectsRenderInterfaceContextFeature? _externalObjectsFeature; public GlSkiaGpu(IGlContext context, long? maxResourceBytes) { @@ -32,6 +35,9 @@ namespace Avalonia.Skia _grContext.SetResourceCacheLimit(maxResourceBytes.Value); } } + + context.TryGetFeature(out var externalObjects); + _externalObjectsFeature = new GlSkiaExternalObjectsFeature(this, externalObjects); } } @@ -103,8 +109,34 @@ namespace Avalonia.Skia public IGlContext CreateSharedContext(IEnumerable preferredVersions = null) => _glContext.CreateSharedContext(preferredVersions); - public IOpenGlBitmapImpl CreateOpenGlBitmap(PixelSize size, Vector dpi) => new GlOpenGlBitmapImpl(_glContext, size, dpi); - + public ICompositionImportableOpenGlSharedTexture CreateSharedTextureForComposition(IGlContext context, PixelSize size) + { + if (!context.IsSharedWith(_glContext)) + throw new InvalidOperationException("Contexts do not belong to the same share group"); + + using (context.EnsureCurrent()) + { + var gl = context.GlInterface; + gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out int oldTexture); + var tex = gl.GenTexture(); + + var format = context.Version.Type == GlProfileType.OpenGLES && context.Version.Major == 2 + ? GL_RGBA + : GL_RGBA8; + + gl.BindTexture(GL_TEXTURE_2D, tex); + gl.TexImage2D(GL_TEXTURE_2D, 0, + format, size.Width, size.Height, + 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); + + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + gl.BindTexture(GL_TEXTURE_2D, oldTexture); + + return new GlSkiaSharedTextureForComposition(context, tex, format, size); + } + } + public void Dispose() { if (_glContext.IsLost) @@ -125,6 +157,8 @@ namespace Avalonia.Skia { if (featureType == typeof(IOpenGlTextureSharingRenderInterfaceContextFeature)) return this; + if (featureType == typeof(IExternalObjectsRenderInterfaceContextFeature)) + return _externalObjectsFeature; return null; } diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs new file mode 100644 index 0000000000..2d53254500 --- /dev/null +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaSharedTextureForComposition.cs @@ -0,0 +1,44 @@ +using Avalonia.OpenGL; + +namespace Avalonia.Skia; + +internal class GlSkiaSharedTextureForComposition : ICompositionImportableOpenGlSharedTexture +{ + public IGlContext Context { get; } + private readonly object _lock = new(); + + public GlSkiaSharedTextureForComposition(IGlContext context, int textureId, int internalFormat, PixelSize size) + { + Context = context; + TextureId = textureId; + InternalFormat = internalFormat; + Size = size; + } + public void Dispose(IGlContext context) + { + lock (_lock) + { + if(TextureId == 0) + return; + try + { + using (context.EnsureCurrent()) + context.GlInterface.DeleteTexture(TextureId); + } + catch + { + // Ignore + } + + TextureId = 0; + } + } + + public int TextureId { get; private set; } + public int InternalFormat { get; } + public PixelSize Size { get; } + public void Dispose() + { + Dispose(Context); + } +} \ No newline at end of file diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs deleted file mode 100644 index d8bff7cfc8..0000000000 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/OpenGlBitmapImpl.cs +++ /dev/null @@ -1,210 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; -using Avalonia.Utilities; -using SkiaSharp; -using static Avalonia.OpenGL.GlConsts; - -namespace Avalonia.Skia -{ - class GlOpenGlBitmapImpl : IOpenGlBitmapImpl, IDrawableBitmapImpl - { - private readonly IGlContext _context; - private readonly object _lock = new object(); - private IGlPresentableOpenGlSurface _surface; - - public GlOpenGlBitmapImpl(IGlContext context, PixelSize pixelSize, Vector dpi) - { - _context = context; - PixelSize = pixelSize; - Dpi = dpi; - } - - public Vector Dpi { get; } - public PixelSize PixelSize { get; } - public int Version { get; private set; } - public void Save(string fileName, int? quality = null) => throw new NotSupportedException(); - - public void Save(Stream stream, int? quality = null) => throw new NotSupportedException(); - - public void Draw(DrawingContextImpl context, SKRect sourceRect, SKRect destRect, SKPaint paint) - { - lock (_lock) - { - if (_surface == null) - return; - using (_surface.Lock()) - { - using (var backendTexture = new GRBackendTexture(PixelSize.Width, PixelSize.Height, false, - new GRGlTextureInfo( - GlConsts.GL_TEXTURE_2D, (uint)_surface.GetTextureId(), - (uint)_surface.InternalFormat))) - using (var surface = SKSurface.Create(context.GrContext, backendTexture, GRSurfaceOrigin.BottomLeft, - SKColorType.Rgba8888, new SKSurfaceProperties(SKPixelGeometry.RgbHorizontal))) - { - // Again, silently ignore, if something went wrong it's not our fault - if (surface == null) - return; - - using (var snapshot = surface.Snapshot()) - context.Canvas.DrawImage(snapshot, sourceRect, destRect, paint); - } - - } - } - } - - public IOpenGlBitmapAttachment CreateFramebufferAttachment(IGlContext context, Action presentCallback) - { - if (!SupportsContext(context)) - throw new OpenGlException("Context is not supported for texture sharing"); - return new SharedOpenGlBitmapAttachment(this, context, presentCallback); - } - - public bool SupportsContext(IGlContext context) - { - // TODO: negotiated platform surface sharing - return _context.IsSharedWith(context); - } - - public void Dispose() - { - - } - - internal void Present(IGlPresentableOpenGlSurface surface) - { - lock (_lock) - { - _surface = surface; - } - } - } - - interface IGlPresentableOpenGlSurface : IDisposable - { - int GetTextureId(); - int InternalFormat { get; } - IDisposable Lock(); - } - - class SharedOpenGlBitmapAttachment : IOpenGlBitmapAttachment, IGlPresentableOpenGlSurface - { - private readonly GlOpenGlBitmapImpl _bitmap; - private readonly IGlContext _context; - private readonly Action _presentCallback; - private readonly int _fbo; - private readonly int _texture; - private readonly int _frontBuffer; - private bool _disposed; - private readonly DisposableLock _lock = new DisposableLock(); - - public unsafe SharedOpenGlBitmapAttachment(GlOpenGlBitmapImpl bitmap, IGlContext context, Action presentCallback) - { - _bitmap = bitmap; - _context = context; - _presentCallback = presentCallback; - using (_context.EnsureCurrent()) - { - var glVersion = _context.Version; - InternalFormat = glVersion.Type == GlProfileType.OpenGLES && glVersion.Major == 2 - ? GL_RGBA - : GL_RGBA8; - - _context.GlInterface.GetIntegerv(GL_FRAMEBUFFER_BINDING, out _fbo); - if (_fbo == 0) - throw new OpenGlException("Current FBO is 0"); - - { - var gl = _context.GlInterface; - - Span textures = stackalloc int[2]; - fixed (int* ptex = textures) - gl.GenTextures(2, ptex); - _texture = textures[0]; - _frontBuffer = textures[1]; - - gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); - foreach (var t in textures) - { - gl.BindTexture(GL_TEXTURE_2D, t); - gl.TexImage2D(GL_TEXTURE_2D, 0, - InternalFormat, - _bitmap.PixelSize.Width, _bitmap.PixelSize.Height, - 0, GL_RGBA, GL_UNSIGNED_BYTE, IntPtr.Zero); - - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - gl.TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - } - - gl.FramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); - gl.BindTexture(GL_TEXTURE_2D, oldTexture); - } - } - } - - public void Present() - { - using (_context.EnsureCurrent()) - { - if (_disposed) - throw new ObjectDisposedException(nameof(SharedOpenGlBitmapAttachment)); - - var gl = _context.GlInterface; - - gl.Finish(); - using (Lock()) - { - gl.GetIntegerv(GL_FRAMEBUFFER_BINDING, out var oldFbo); - gl.GetIntegerv(GL_TEXTURE_BINDING_2D, out var oldTexture); - gl.GetIntegerv(GL_ACTIVE_TEXTURE, out var oldActive); - - gl.BindFramebuffer(GL_FRAMEBUFFER, _fbo); - gl.ActiveTexture(GL_TEXTURE0); - gl.BindTexture(GL_TEXTURE_2D, _frontBuffer); - - gl.CopyTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 0, 0, _bitmap.PixelSize.Width, - _bitmap.PixelSize.Height); - - gl.BindFramebuffer(GL_FRAMEBUFFER, oldFbo); - gl.ActiveTexture(oldActive); - gl.BindTexture(GL_TEXTURE_2D, oldTexture); - - gl.Finish(); - } - } - - _bitmap.Present(this); - _presentCallback(); - } - - public unsafe void Dispose() - { - var gl = _context.GlInterface; - _bitmap.Present(null); - - if(_disposed) - return; - using (_context.MakeCurrent()) - using (Lock()) - { - if(_disposed) - return; - _disposed = true; - var ptex = stackalloc[] { _texture, _frontBuffer }; - gl.DeleteTextures(2, ptex); - } - } - - int IGlPresentableOpenGlSurface.GetTextureId() - { - return _frontBuffer; - } - - public int InternalFormat { get; } - - public IDisposable Lock() => _lock.Lock(); - } -} diff --git a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs index e24d805050..802736119f 100644 --- a/src/Skia/Avalonia.Skia/ImmutableBitmap.cs +++ b/src/Skia/Avalonia.Skia/ImmutableBitmap.cs @@ -37,6 +37,13 @@ namespace Avalonia.Skia } } + public ImmutableBitmap(SKImage image) + { + _image = image; + PixelSize = new PixelSize(image.Width, image.Height); + Dpi = new Vector(96, 96); + } + public ImmutableBitmap(ImmutableBitmap src, PixelSize destinationSize, BitmapInterpolationMode interpolationMode) { SKImageInfo info = new SKImageInfo(destinationSize.Width, destinationSize.Height, SKColorType.Bgra8888); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index b202b60cdf..23b7edcc8f 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -2,13 +2,8 @@ using System; using System.Collections; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Threading; - -using Avalonia.Controls.Platform.Surfaces; using Avalonia.Media; using Avalonia.OpenGL; -using Avalonia.OpenGL.Imaging; using Avalonia.Platform; using Avalonia.Media.Imaging; using SkiaSharp; diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs index 4949f4a50d..0cf66767cb 100644 --- a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -43,5 +43,7 @@ internal class SkiaContext : IPlatformRenderInterfaceContext "Don't know how to create a Skia render target from any of provided surfaces"); } + public bool IsLost => _gpu.IsLost; + public object TryGetFeature(Type featureType) => _gpu?.TryGetFeature(featureType); -} \ No newline at end of file +} diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 5887ba2172..25c351c908 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -238,6 +238,7 @@ namespace Avalonia.Direct2D1 } public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => _platform.CreateRenderTarget(surfaces); + public bool IsLost => false; } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs index a749ed1b45..1357e8aa63 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXEnums.cs @@ -52,6 +52,44 @@ namespace Avalonia.Win32.DirectX D3D11_USAGE_STAGING = 3, } + + [Flags] + internal enum D3D11_RESOURCE_MISC_FLAG + { + D3D11_RESOURCE_MISC_GENERATE_MIPS = 0x00000001, + D3D11_RESOURCE_MISC_SHARED = 0x00000002, + D3D11_RESOURCE_MISC_TEXTURECUBE = 0x00000004, + D3D11_RESOURCE_MISC_DRAWINDIRECT_ARGS = 0x00000010, + D3D11_RESOURCE_MISC_BUFFER_ALLOW_RAW_VIEWS = 0x00000020, + D3D11_RESOURCE_MISC_BUFFER_STRUCTURED = 0x00000040, + D3D11_RESOURCE_MISC_RESOURCE_CLAMP = 0x00000080, + D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX = 0x00000100, + D3D11_RESOURCE_MISC_GDI_COMPATIBLE = 0x00000200, + D3D11_RESOURCE_MISC_SHARED_NTHANDLE = 0x00000800, + D3D11_RESOURCE_MISC_RESTRICTED_CONTENT = 0x00001000, + D3D11_RESOURCE_MISC_RESTRICT_SHARED_RESOURCE = 0x00002000, + D3D11_RESOURCE_MISC_RESTRICT_SHARED_RESOURCE_DRIVER = 0x00004000, + D3D11_RESOURCE_MISC_GUARDED = 0x00008000, + D3D11_RESOURCE_MISC_TILE_POOL = 0x00020000, + D3D11_RESOURCE_MISC_TILED = 0x00040000, + D3D11_RESOURCE_MISC_HW_PROTECTED = 0x00080000, + } + + [Flags] + internal enum D3D11_BIND_FLAG + { + D3D11_BIND_VERTEX_BUFFER = 0x00000001, + D3D11_BIND_INDEX_BUFFER = 0x00000002, + D3D11_BIND_CONSTANT_BUFFER = 0x00000004, + D3D11_BIND_SHADER_RESOURCE = 0x00000008, + D3D11_BIND_STREAM_OUTPUT = 0x00000010, + D3D11_BIND_RENDER_TARGET = 0x00000020, + D3D11_BIND_DEPTH_STENCIL = 0x00000040, + D3D11_BIND_UNORDERED_ACCESS = 0x00000080, + D3D11_BIND_DECODER = 0x00000200, + D3D11_BIND_VIDEO_ENCODER = 0x00000400, + } + internal enum DXGI_SWAP_EFFECT { DXGI_SWAP_EFFECT_DISCARD = 0, diff --git a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs index 47451831a6..c11a6026e7 100644 --- a/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs +++ b/src/Windows/Avalonia.Win32/DirectX/DirectXStructs.cs @@ -282,11 +282,11 @@ namespace Avalonia.Win32.DirectX public D3D11_USAGE Usage; - public uint BindFlags; + public D3D11_BIND_FLAG BindFlags; public uint CPUAccessFlags; - public uint MiscFlags; + public D3D11_RESOURCE_MISC_FLAG MiscFlags; } #nullable restore } diff --git a/src/Windows/Avalonia.Win32/DirectX/directx.idl b/src/Windows/Avalonia.Win32/DirectX/directx.idl index a4552eedea..1d66c898db 100644 --- a/src/Windows/Avalonia.Win32/DirectX/directx.idl +++ b/src/Windows/Avalonia.Win32/DirectX/directx.idl @@ -10,6 +10,7 @@ @clr-map SIZE Avalonia.Win32.Interop.UnmanagedMethods.SIZE @clr-map POINT Avalonia.Win32.Interop.UnmanagedMethods.POINT @clr-map HWND IntPtr +@clr-map HANDLE IntPtr @clr-map BOOL int @clr-map DWORD int @clr-map SIZE_T IntPtr @@ -259,10 +260,28 @@ interface IDXGISurface : IDXGIDeviceSubObject HRESULT Unmap(); } +[uuid( 035f3ab4-482e-4e50-b41f-8a7f8bd8960b)] +interface IDXGIResource : IDXGIDeviceSubObject +{ + HRESULT GetSharedHandle( [out, annotation("_Out_")] HANDLE * pSharedHandle ); + HRESULT GetUsage( [out] DXGI_USAGE * pUsage ); + HRESULT SetEvictionPriority( [in] UINT EvictionPriority ); + HRESULT GetEvictionPriority( [out, retval, annotation("_Out_")] UINT* pEvictionPriority ); +}; + + +[ uuid( 9d8e1289-d7b3-465f-8126-250e349af85d)] +interface IDXGIKeyedMutex : + IDXGIDeviceSubObject +{ + HRESULT AcquireSync( [in] UINT64 Key, [in] uint dwMilliseconds); + HRESULT ReleaseSync( [in] UINT64 Key); +}; + [uuid(770aae78-f26f-4dba-a829-253c83d1b387)] interface IDXGIFactory1 : IDXGIFactory { - HRESULT EnumAdapters1([in] UINT Adapter, [out, annotation("_COM_Outptr_")] IDXGIAdapter1** ppAdapter); + int EnumAdapters1([in] UINT Adapter, [out] void** ppAdapter); BOOL IsCurrent(); } @@ -339,9 +358,9 @@ interface ID3D11Device : IUnknown IntPtr pInitialData, [out, retval] IUnknown** ppTexture1D ); HRESULT CreateTexture2D( - IntPtr pDesc, + D3D11_TEXTURE2D_DESC* pDesc, IntPtr pInitialData, - [out, retval] IUnknown** ppTexture2D ); + [out, retval] ID3D11Texture2D** ppTexture2D ); HRESULT CreateTexture3D( IntPtr pDesc, IntPtr pInitialData, @@ -481,3 +500,11 @@ interface ID3D11Device : IUnknown HRESULT SetExceptionMode( UINT RaiseFlags ); UINT GetExceptionMode(); } + + +[uuid( 6f15aaf2-d208-4e89-9ab4-489535d34f9c)] +interface ID3D11Texture2D : IUnknown +{ + // Just a marker interface for now +}; + diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs new file mode 100644 index 0000000000..b0fa76fda2 --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalD3D11Texture2D.cs @@ -0,0 +1,107 @@ +#nullable enable +using System; +using System.Threading; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Angle; +using Avalonia.OpenGL.Egl; +using Avalonia.Platform; +using Avalonia.Win32.DirectX; +using MicroCom.Runtime; +using static Avalonia.OpenGL.Egl.EglConsts; +using static Avalonia.OpenGL.GlConsts; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleExternalMemoryD3D11Texture2D : IGlExternalImageTexture +{ + private readonly EglContext _context; + private ID3D11Texture2D _texture2D; + private EglSurface _eglSurface; + private IDXGIKeyedMutex _mutex; + + public unsafe AngleExternalMemoryD3D11Texture2D(EglContext context, ID3D11Texture2D texture2D, PlatformGraphicsExternalImageProperties props) + { + _context = context; + _texture2D = texture2D.CloneReference(); + _mutex = _texture2D.QueryInterface(); + Properties = props; + + InternalFormat = GL_RGBA8; + + _eglSurface = _context.Display.CreatePBufferFromClientBuffer(EGL_D3D_TEXTURE_ANGLE, texture2D.GetNativeIntPtr(), + new[] + { + EGL_WIDTH, props.Width, EGL_HEIGHT, props.Height, EGL_TEXTURE_FORMAT, EGL_TEXTURE_RGBA, + EGL_TEXTURE_TARGET, EGL_TEXTURE_2D, EGL_TEXTURE_INTERNAL_FORMAT_ANGLE, GL_RGBA, EGL_NONE, EGL_NONE, + EGL_NONE + }); + + var gl = _context.GlInterface; + int temp = 0; + gl.GenTextures(1, &temp); + TextureId = temp; + gl.BindTexture(GlConsts.GL_TEXTURE_2D, TextureId); + + if (_context.Display.EglInterface.BindTexImage(_context.Display.Handle, _eglSurface.DangerousGetHandle(), + EGL_BACK_BUFFER) == 0) + + throw OpenGlException.GetFormattedException("eglBindTexImage", _context.Display.EglInterface); + } + + public void Dispose() + { + + if (!_context.IsLost && TextureId != 0) + using (_context.EnsureCurrent()) + _context.GlInterface.DeleteTexture(TextureId); + TextureId = 0; + _eglSurface?.Dispose(); + _eglSurface = null!; + _texture2D?.Dispose(); + _texture2D = null!; + _mutex?.Dispose(); + _mutex = null!; + } + + + public void AcquireKeyedMutex(uint key) => _mutex.AcquireSync(key, int.MaxValue); + + public void ReleaseKeyedMutex(uint key) => _mutex.ReleaseSync(key); + + public int TextureId { get; private set; } + public int InternalFormat { get; } + public PlatformGraphicsExternalImageProperties Properties { get; } +} + +internal class AngleExternalMemoryD3D11ExportedTexture2D : AngleExternalMemoryD3D11Texture2D, IGlExportableExternalImageTexture +{ + static IPlatformHandle GetHandle(ID3D11Texture2D texture2D) + { + using var resource = texture2D.QueryInterface(); + return new PlatformHandle(resource.SharedHandle, + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle); + } + + public AngleExternalMemoryD3D11ExportedTexture2D(EglContext context, ID3D11Texture2D texture2D, + D3D11_TEXTURE2D_DESC desc, + PlatformGraphicsExternalImageFormat format) + : this(context, texture2D, GetHandle(texture2D), + new PlatformGraphicsExternalImageProperties + { + Width = (int)desc.Width, Height = (int)desc.Height, Format = format + }) + { + + } + + private AngleExternalMemoryD3D11ExportedTexture2D(EglContext context, ID3D11Texture2D texture2D, + IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) + : base(context, texture2D, properties) + { + Handle = handle; + } + + public IPlatformHandle Handle { get; } + public IPlatformHandle GetHandle() => Handle; + +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs new file mode 100644 index 0000000000..a7b52e953e --- /dev/null +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleExternalObjectsFeature.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Controls.Documents; +using Avalonia.OpenGL; +using Avalonia.OpenGL.Egl; +using Avalonia.Platform; +using Avalonia.Rendering.Composition; +using Avalonia.Win32.DirectX; +using MicroCom.Runtime; + +namespace Avalonia.Win32.OpenGl.Angle; + +internal class AngleExternalObjectsFeature : IGlContextExternalObjectsFeature, IDisposable +{ + private readonly EglContext _context; + private readonly ID3D11Device _device; + + public AngleExternalObjectsFeature(EglContext context) + { + _context = context; + var angle = (AngleWin32EglDisplay)context.Display; + _device = MicroComRuntime.CreateProxyFor(angle.GetDirect3DDevice(), false).CloneReference(); + using var dxgiDevice = _device.QueryInterface(); + using var adapter = dxgiDevice.Adapter; + DeviceLuid = BitConverter.GetBytes(adapter.Desc.AdapterLuid); + } + + public IReadOnlyList SupportedImportableExternalImageTypes { get; } = new[] + { + KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle + }; + + public IReadOnlyList SupportedExportableExternalImageTypes => SupportedImportableExternalImageTypes; + public IReadOnlyList SupportedImportableExternalSemaphoreTypes => Array.Empty(); + public IReadOnlyList SupportedExportableExternalSemaphoreTypes => Array.Empty(); + + public IReadOnlyList GetSupportedFormatsForExternalMemoryType(string type) => + new[] + { + PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm + }; + + public unsafe IGlExportableExternalImageTexture CreateImage(string type, PixelSize size, + PlatformGraphicsExternalImageFormat format) + { + if (format != PlatformGraphicsExternalImageFormat.R8G8B8A8UNorm) + throw new NotSupportedException("Unsupported external memory format"); + using (_context.EnsureCurrent()) + { + var fmt = DXGI_FORMAT.DXGI_FORMAT_R8G8B8A8_UNORM; + + var desc = new D3D11_TEXTURE2D_DESC + { + Format = fmt, + Width = (uint)size.Width, + Height = (uint)size.Height, + ArraySize = 1, + MipLevels = 1, + SampleDesc = new DXGI_SAMPLE_DESC { Count = 1, Quality = 0 }, + Usage = D3D11_USAGE.D3D11_USAGE_DEFAULT, + CPUAccessFlags = 0, + MiscFlags = D3D11_RESOURCE_MISC_FLAG.D3D11_RESOURCE_MISC_SHARED_KEYEDMUTEX, + BindFlags = D3D11_BIND_FLAG.D3D11_BIND_RENDER_TARGET | D3D11_BIND_FLAG.D3D11_BIND_SHADER_RESOURCE, + }; + using var texture = _device.CreateTexture2D(&desc, IntPtr.Zero); + return new AngleExternalMemoryD3D11ExportedTexture2D(_context, texture, desc, format); + } + } + + public IGlExportableExternalImageTexture CreateSemaphore(string type) => throw new NotSupportedException(); + + public unsafe IGlExternalImageTexture ImportImage(IPlatformHandle handle, PlatformGraphicsExternalImageProperties properties) + { + if (handle.HandleDescriptor != KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle) + throw new NotSupportedException("Unsupported external memory type"); + + using (_context.EnsureCurrent()) + { + var guid = MicroComRuntime.GetGuidFor(typeof(ID3D11Texture2D)); + using var opened = _device.OpenSharedResource(handle.Handle, &guid); + using var texture = opened.QueryInterface(); + return new AngleExternalMemoryD3D11Texture2D(_context, texture, properties); + } + } + + public IGlExternalSemaphore ImportSemaphore(IPlatformHandle handle) => throw new NotSupportedException(); + public CompositionGpuImportedImageSynchronizationCapabilities GetSynchronizationCapabilities(string imageHandleType) + { + if (imageHandleType == KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureGlobalSharedHandle + || imageHandleType == KnownPlatformGraphicsExternalImageHandleTypes.D3D11TextureNtHandle) + return CompositionGpuImportedImageSynchronizationCapabilities.KeyedMutex; + return default; + } + + public byte[] DeviceLuid { get; } + public byte[] DeviceUuid { get; } + + public void Dispose() + { + _device.Dispose(); + } +} diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs index b2d7b2014b..53bf2fb8b1 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32EglDisplay.cs @@ -1,5 +1,8 @@ +#nullable enable annotations using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Runtime.InteropServices; using Avalonia.OpenGL; using Avalonia.OpenGL.Angle; @@ -7,6 +10,7 @@ using Avalonia.OpenGL.Egl; using Avalonia.Win32.DirectX; using MicroCom.Runtime; using static Avalonia.OpenGL.Egl.EglConsts; +// ReSharper disable SimplifyLinqExpressionUseMinByAndMaxBy namespace Avalonia.Win32.OpenGl.Angle { @@ -40,51 +44,90 @@ namespace Avalonia.Win32.OpenGl.Angle }, AngleOptions.PlatformApi.DirectX11); } - public static AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl) + public static unsafe AngleWin32EglDisplay CreateD3D11Display(Win32AngleEglInterface egl, + bool preferDiscreteAdapter = false) { - unsafe + var featureLevels = new[] { - var featureLevels = new[] - { - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_1, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_0, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_3, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_2, - D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_1 - }; - - DirectXUnmanagedMethods.D3D11CreateDevice(IntPtr.Zero, D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_HARDWARE, - IntPtr.Zero, 0, featureLevels, (uint)featureLevels.Length, - 7, out var pD3dDevice, out var featureLevel, null); - if (pD3dDevice == IntPtr.Zero) - throw new Win32Exception("Unable to create D3D11 Device"); + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_11_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_1, D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_10_0, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_3, D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_2, + D3D_FEATURE_LEVEL.D3D_FEATURE_LEVEL_9_1 + }; - var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); - var angleDevice = IntPtr.Zero; - var display = IntPtr.Zero; + var dxgiFactoryGuid = MicroComRuntime.GetGuidFor(typeof(IDXGIFactory1)); + DirectXUnmanagedMethods.CreateDXGIFactory1(ref dxgiFactoryGuid, out var pDxgiFactory); + IDXGIAdapter1? chosenAdapter = null; + if (pDxgiFactory != null) + { + using var factory = MicroComRuntime.CreateProxyFor(pDxgiFactory, true); - void Cleanup() + void* pAdapter = null; + if (preferDiscreteAdapter) { - if (angleDevice != IntPtr.Zero) - egl.ReleaseDeviceANGLE(angleDevice); - d3dDevice.Dispose(); + ushort adapterIndex = 0; + var adapters = new List<(IDXGIAdapter1 adapter, string name)>(); + while (factory.EnumAdapters1(adapterIndex, &pAdapter) == 0) + { + var adapter = MicroComRuntime.CreateProxyFor(pAdapter, true); + var desc = adapter.Desc1; + var name = Marshal.PtrToStringUni(new IntPtr(desc.Description))!.ToLowerInvariant(); + adapters.Add((adapter, name)); + adapterIndex++; + } + + if (adapters.Count == 0) + throw new OpenGlException("No adapters found"); + chosenAdapter = adapters + .OrderByDescending(x => x.name.Contains("nvidia") ? 2 : x.name.Contains("amd") ? 1 : 0) + .First().adapter.CloneReference(); + foreach (var a in adapters) + a.adapter.Dispose(); } - - bool success = false; - try + else { - angleDevice = egl.CreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, pD3dDevice, null); - if (angleDevice == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglCreateDeviceANGLE", egl); + if (factory.EnumAdapters1(0, &pAdapter) != 0) + throw new OpenGlException("No adapters found"); + chosenAdapter = MicroComRuntime.CreateProxyFor(pAdapter, true); + } + } + + IntPtr pD3dDevice; + using (chosenAdapter) + DirectXUnmanagedMethods.D3D11CreateDevice(chosenAdapter?.GetNativeIntPtr() ?? IntPtr.Zero, + D3D_DRIVER_TYPE.D3D_DRIVER_TYPE_UNKNOWN, + IntPtr.Zero, 0, featureLevels, (uint)featureLevels.Length, + 7, out pD3dDevice, out var featureLevel, null); + - display = egl.GetPlatformDisplayExt(EGL_PLATFORM_DEVICE_EXT, angleDevice, null); - if (display == IntPtr.Zero) - throw OpenGlException.GetFormattedException("eglGetPlatformDisplayEXT", egl); + if (pD3dDevice == IntPtr.Zero) + throw new Win32Exception("Unable to create D3D11 Device"); + var d3dDevice = MicroComRuntime.CreateProxyFor(pD3dDevice, true); + var angleDevice = IntPtr.Zero; + var display = IntPtr.Zero; - var rv = new AngleWin32EglDisplay(display, new EglDisplayOptions + void Cleanup() + { + if (angleDevice != IntPtr.Zero) + egl.ReleaseDeviceANGLE(angleDevice); + d3dDevice.Dispose(); + } + + bool success = false; + try + { + angleDevice = egl.CreateDeviceANGLE(EGL_D3D11_DEVICE_ANGLE, pD3dDevice, null); + if (angleDevice == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateDeviceANGLE", egl); + + display = egl.GetPlatformDisplayExt(EGL_PLATFORM_DEVICE_EXT, angleDevice, null); + if (display == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglGetPlatformDisplayEXT", egl); + + + var rv = new AngleWin32EglDisplay(display, + new EglDisplayOptions { DisposeCallback = Cleanup, Egl = egl, @@ -92,17 +135,16 @@ namespace Avalonia.Win32.OpenGl.Angle DeviceLostCheckCallback = () => d3dDevice.DeviceRemovedReason != 0, GlVersions = AvaloniaLocator.Current.GetService()?.GlProfiles }, AngleOptions.PlatformApi.DirectX11); - success = true; - return rv; - } - finally + success = true; + return rv; + } + finally + { + if (!success) { - if (!success) - { - if (display != IntPtr.Zero) - egl.Terminate(display); - Cleanup(); - } + if (display != IntPtr.Zero) + egl.Terminate(display); + Cleanup(); } } } diff --git a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs index 9a829aff92..a4d1ea457a 100644 --- a/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs +++ b/src/Windows/Avalonia.Win32/OpenGl/Angle/AngleWin32PlatformGraphics.cs @@ -9,7 +9,7 @@ using Avalonia.Platform; namespace Avalonia.Win32.OpenGl.Angle; -internal class AngleWin32PlatformGraphics : IPlatformGraphics +internal class AngleWin32PlatformGraphics : IPlatformGraphics, IPlatformGraphicsOpenGlContextFactory { private readonly Win32AngleEglInterface _egl; private AngleWin32EglDisplay _sharedDisplay; @@ -29,9 +29,10 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics var rv = display.CreateContext(new EglContextOptions { DisposeCallback = display.Dispose, - ExtraFeatures = new Dictionary + ExtraFeatures = new Dictionary> { - [typeof(IGlPlatformSurfaceRenderTargetFactory)] = new AngleD3DTextureFeature() + [typeof(IGlPlatformSurfaceRenderTargetFactory)] = _ => new AngleD3DTextureFeature(), + [typeof(IGlContextExternalObjectsFeature)] = context => new AngleExternalObjectsFeature(context) } }); success = true; @@ -73,8 +74,6 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics public static AngleWin32PlatformGraphics TryCreate(AngleOptions options) { - - Win32AngleEglInterface egl; try { @@ -128,4 +127,13 @@ internal class AngleWin32PlatformGraphics : IPlatformGraphics } return null; } + + public IGlContext CreateContext(IEnumerable? versions) + { + if (UsesSharedContext) + throw new InvalidOperationException(); + if (versions != null && versions.All(v => v.Type != GlProfileType.OpenGLES || v.Major != 3)) + throw new OpenGlException("Unable to create context with requested version"); + return (IGlContext)CreateContext(); + } } diff --git a/src/Windows/Avalonia.Win32/Win32GlManager.cs b/src/Windows/Avalonia.Win32/Win32GlManager.cs index 25ea060576..ab6de1a027 100644 --- a/src/Windows/Avalonia.Win32/Win32GlManager.cs +++ b/src/Windows/Avalonia.Win32/Win32GlManager.cs @@ -37,6 +37,9 @@ namespace Avalonia.Win32 if (egl != null && egl.PlatformApi == AngleOptions.PlatformApi.DirectX11) { + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(egl); + if (opts.UseWindowsUIComposition) { WinUiCompositorConnection.TryCreateAndRegister(); diff --git a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs index 42e33729ac..6e0d7f657a 100644 --- a/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs +++ b/tests/Avalonia.Base.UnitTests/VisualTree/MockRenderInterface.cs @@ -15,6 +15,8 @@ namespace Avalonia.Base.UnitTests.VisualTree throw new NotImplementedException(); } + public bool IsLost => false; + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index a272d89b8a..5b28453c17 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -46,6 +46,8 @@ namespace Avalonia.Benchmarks throw new NotImplementedException(); } + public bool IsLost => false; + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index f9e1e45098..143937307e 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -57,6 +57,8 @@ namespace Avalonia.UnitTests return new MockRenderTarget(); } + public bool IsLost => false; + public object TryGetFeature(Type featureType) => null; public IRenderTargetBitmapImpl CreateRenderTargetBitmap(PixelSize size, Vector dpi) From 96038db08fd908a3a25ca223ecdaf539fb21b5c1 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Thu, 19 Jan 2023 16:12:00 +0100 Subject: [PATCH 15/23] fix: ItemTemplate is not applied on Menu/MenuItem --- src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml | 1 + src/Avalonia.Themes.Simple/Controls/MenuItem.xaml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml index e87f4b7d93..72a7797bc3 100644 --- a/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml @@ -92,6 +92,7 @@ + Content="{TemplateBinding Header}" + ContentTemplate="{TemplateBinding ItemTemplate}"> From f2908c6c79075bdbeb6a3b19a72a789f4245289b Mon Sep 17 00:00:00 2001 From: cristinathoughtpennies Date: Thu, 19 Jan 2023 10:56:24 -0600 Subject: [PATCH 16/23] HeaderDoubleTapped in TreeViewItem is now protected virtual instead of private --- src/Avalonia.Controls/TreeViewItem.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index fb20a7ee14..532d0e32db 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -264,6 +264,15 @@ namespace Avalonia.Controls Dispatcher.UIThread.Post(this.BringIntoView); // must use the Dispatcher, otherwise the TreeView doesn't scroll } } + + protected virtual void HeaderDoubleTapped(object? sender, TappedEventArgs e) + { + if (ItemCount > 0) + { + IsExpanded = !IsExpanded; + e.Handled = true; + } + } private static int CalculateDistanceFromLogicalParent(ILogical? logical, int @default = -1) where T : class { @@ -277,14 +286,5 @@ namespace Avalonia.Controls return logical != null ? result : @default; } - - private void HeaderDoubleTapped(object? sender, TappedEventArgs e) - { - if (ItemCount > 0) - { - IsExpanded = !IsExpanded; - e.Handled = true; - } - } } } From 89a78f557b13f61b06c6eb56915b7f1a077808e7 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Thu, 19 Jan 2023 18:39:25 +0100 Subject: [PATCH 17/23] Don't keep the text layout buffers around if they're too large --- .../Media/TextFormatting/BidiReorderer.cs | 4 +- .../TextFormatting/FormattingBufferHelper.cs | 62 +++++++++++++++++++ .../TextFormatting/FormattingObjectPool.cs | 2 +- .../Media/TextFormatting/TextFormatterImpl.cs | 3 + .../TextFormatting/Unicode/BiDiAlgorithm.cs | 53 ++++++++++++++-- .../Media/TextFormatting/Unicode/BiDiData.cs | 22 +++++-- src/Avalonia.Base/Utilities/BidiDictionary.cs | 29 ++++----- .../Text/HugeTextLayout.cs | 8 +-- 8 files changed, 150 insertions(+), 33 deletions(-) create mode 100644 src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs index 2c6db4b753..4db55fae6d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs +++ b/src/Avalonia.Base/Media/TextFormatting/BidiReorderer.cs @@ -117,8 +117,8 @@ namespace Avalonia.Media.TextFormatting } finally { - _runs.Clear(); - _ranges.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _runs); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _ranges); } } diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs new file mode 100644 index 0000000000..0341842cb6 --- /dev/null +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + internal static class FormattingBufferHelper + { + // 1MB, arbitrary, that's 512K characters or 128K object references on x64 + private const long MaxKeptBufferSizeInBytes = 1024 * 1024; + + public static void ClearThenResetIfTooLarge(ref ArrayBuilder arrayBuilder) + { + arrayBuilder.Clear(); + + if (IsBufferTooLarge(arrayBuilder.Capacity)) + { + arrayBuilder = default; + } + } + + public static void ClearThenResetIfTooLarge(List list) + { + list.Clear(); + + if (IsBufferTooLarge(list.Capacity)) + { + list.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(Stack stack) + { + stack.Clear(); + + if (IsBufferTooLarge(stack.Count)) + { + stack.TrimExcess(); + } + } + + public static void ClearThenResetIfTooLarge(ref Dictionary dictionary) + where TKey : notnull + { + dictionary.Clear(); + + // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here + if (IsBufferTooLarge>(dictionary.Count)) + { +#if NET6_0_OR_GREATER + dictionary.TrimExcess(); +#else + dictionary = new Dictionary(); +#endif + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static bool IsBufferTooLarge(int length) + => (long)Unsafe.SizeOf() * length > MaxKeptBufferSizeInBytes; + } +} diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs index 0468d8f413..cb8168e693 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingObjectPool.cs @@ -80,7 +80,7 @@ namespace Avalonia.Media.TextFormatting } --_pendingReturnCount; - rentedList.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(rentedList); if (_size < MaxSize) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index c25d530472..bf9f6f77f8 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -225,6 +225,9 @@ namespace Avalonia.Media.TextFormatting CoalesceLevels(textRuns, bidiAlgorithm.ResolvedLevels.Span, processedRuns); + bidiData.Reset(); + bidiAlgorithm.Reset(); + var groupedRuns = objectPool.UnshapedTextRunLists.Rent(); for (var index = 0; index < processedRuns.Count; index++) diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index e960a510a9..3a81784152 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -28,6 +28,11 @@ namespace Avalonia.Media.TextFormatting.Unicode /// internal sealed class BidiAlgorithm { + /// + /// Whether the state is clean and can be reused without a reset. + /// + private bool _hasCleanState = true; + /// /// The original BiDiClass classes as provided by the caller /// @@ -226,16 +231,15 @@ namespace Avalonia.Media.TextFormatting.Unicode ArraySlice? outLevels) { // Reset state - _isolatePairs.Clear(); - _workingClassesBuffer.Clear(); - _levelRuns.Clear(); - _resolvedLevelsBuffer.Clear(); + Reset(); if (types.IsEmpty) { return; } + _hasCleanState = false; + // Setup original types and working types _originalClasses = types; _workingClasses = _workingClassesBuffer.Add(types); @@ -1639,6 +1643,47 @@ namespace Avalonia.Media.TextFormatting.Unicode } } + /// + /// Resets the bidi algorithm to a clean state. + /// + public void Reset() + { + if (_hasCleanState) + { + return; + } + + _originalClasses = default; + _pairedBracketTypes = default; + _pairedBracketValues = default; + _hasBrackets = default; + _hasEmbeddings = default; + _hasIsolates = default; + _isolatePairs.ClearThenResetIfTooLarge(); + _workingClasses = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _workingClassesBuffer); + _resolvedLevels = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _resolvedLevelsBuffer); + _paragraphEmbeddingLevel = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_statusStack); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _x9Map); + FormattingBufferHelper.ClearThenResetIfTooLarge(_levelRuns); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _isolatedRunMapping); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingIsolateOpenings); + _runLevel = default; + _runDirection = default; + _runLength = default; + _runResolvedClasses = default; + _runOriginalClasses = default; + _runLevels = default; + _runBiDiPairedBracketTypes = default; + _runPairedBracketValues = default; + FormattingBufferHelper.ClearThenResetIfTooLarge(_pendingOpeningBrackets); + FormattingBufferHelper.ClearThenResetIfTooLarge(_pairedBrackets); + + _hasCleanState = true; + } + /// /// Hold the start and end index of a pair of brackets /// diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 226e5ad6bd..8bd2171a41 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -14,6 +14,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// To avoid allocations, this class is designed to be reused. internal sealed class BidiData { + private bool _hasCleanState = true; private ArrayBuilder _classes; private ArrayBuilder _pairedBracketTypes; private ArrayBuilder _pairedBracketValues; @@ -62,6 +63,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// The text to process. public void Append(ReadOnlySpan text) { + _hasCleanState = false; + _classes.Add(text.Length); _pairedBracketTypes.Add(text.Length); _pairedBracketValues.Add(text.Length); @@ -183,12 +186,17 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void Reset() { - _classes.Clear(); - _pairedBracketTypes.Clear(); - _pairedBracketValues.Clear(); - _savedClasses.Clear(); - _savedPairedBracketTypes.Clear(); - _tempLevelBuffer.Clear(); + if (_hasCleanState) + { + return; + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _classes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _pairedBracketValues); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedClasses); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _savedPairedBracketTypes); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _tempLevelBuffer); ParagraphEmbeddingLevel = 0; HasBrackets = false; @@ -199,6 +207,8 @@ namespace Avalonia.Media.TextFormatting.Unicode Classes = default; PairedBracketTypes = default; PairedBracketValues = default; + + _hasCleanState = true; } } } diff --git a/src/Avalonia.Base/Utilities/BidiDictionary.cs b/src/Avalonia.Base/Utilities/BidiDictionary.cs index 654fbc9807..01af53ad89 100644 --- a/src/Avalonia.Base/Utilities/BidiDictionary.cs +++ b/src/Avalonia.Base/Utilities/BidiDictionary.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Media.TextFormatting; namespace Avalonia.Utilities { @@ -9,32 +11,27 @@ namespace Avalonia.Utilities /// Value type internal sealed class BidiDictionary where T1 : notnull where T2 : notnull { - public Dictionary Forward { get; } = new Dictionary(); + private Dictionary _forward = new(); + private Dictionary _reverse = new(); - public Dictionary Reverse { get; } = new Dictionary(); - - public void Clear() + public void ClearThenResetIfTooLarge() { - Forward.Clear(); - Reverse.Clear(); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _forward); + FormattingBufferHelper.ClearThenResetIfTooLarge(ref _reverse); } public void Add(T1 key, T2 value) { - Forward.Add(key, value); - Reverse.Add(value, key); + _forward.Add(key, value); + _reverse.Add(value, key); } -#pragma warning disable CS8601 - public bool TryGetValue(T1 key, out T2 value) => Forward.TryGetValue(key, out value); -#pragma warning restore CS8601 + public bool TryGetValue(T1 key, [MaybeNullWhen(false)] out T2 value) => _forward.TryGetValue(key, out value); -#pragma warning disable CS8601 - public bool TryGetKey(T2 value, out T1 key) => Reverse.TryGetValue(value, out key); -#pragma warning restore CS8601 + public bool TryGetKey(T2 value, [MaybeNullWhen(false)] out T1 key) => _reverse.TryGetValue(value, out key); - public bool ContainsKey(T1 key) => Forward.ContainsKey(key); + public bool ContainsKey(T1 key) => _forward.ContainsKey(key); - public bool ContainsValue(T2 value) => Reverse.ContainsKey(value); + public bool ContainsValue(T2 value) => _reverse.ContainsKey(value); } } diff --git a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs index 8b23855fde..c96edbef5c 100644 --- a/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs +++ b/tests/Avalonia.Benchmarks/Text/HugeTextLayout.cs @@ -48,10 +48,10 @@ It may reveal how the matters of peculiar interest slowly the goals and objectiv In respect that the structure of the sufficient amount poses problems and challenges for both the set of related commands and controls and the ability bias."; [Params(false, true)] - public bool UseWrapping { get; set; } + public bool Wrap { get; set; } [Params(false, true)] - public bool UseTrimming { get; set; } + public bool Trim { get; set; } [Benchmark] public TextLayout BuildTextLayout() => MakeLayout(Text); @@ -101,8 +101,8 @@ In respect that the structure of the sufficient amount poses problems and challe private TextLayout MakeLayout(string str) { - var wrapping = UseWrapping ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; - var trimming = UseTrimming ? TextTrimming.CharacterEllipsis : TextTrimming.None; + var wrapping = Wrap ? TextWrapping.WrapWithOverflow : TextWrapping.NoWrap; + var trimming = Trim ? TextTrimming.CharacterEllipsis : TextTrimming.None; var layout = new TextLayout(str, Typeface.Default, 12d, Brushes.Black, maxWidth: 120, textTrimming: trimming, textWrapping: wrapping); layout.Dispose(); From 7a2ca3e9998a3d0f61c57f6044246d0861cc844e Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 19 Jan 2023 23:00:51 +0600 Subject: [PATCH 18/23] Build --- samples/GpuInterop/D3DDemo/D3D11Swapchain.cs | 9 --------- samples/GpuInterop/GpuInterop.csproj | 1 + samples/GpuInterop/Program.cs | 4 ++-- src/Avalonia.Base/Rendering/SwapchainBase.cs | 3 +-- src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs | 3 +-- 5 files changed, 5 insertions(+), 15 deletions(-) diff --git a/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs b/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs index 30a4c19d35..d2cf43ac74 100644 --- a/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs +++ b/samples/GpuInterop/D3DDemo/D3D11Swapchain.cs @@ -1,23 +1,14 @@ using System; -using System.Collections.Generic; -using System.Linq; -using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Rendering.Composition; -using SharpDX.Direct2D1; using SharpDX.Direct3D11; using SharpDX.DXGI; -using SharpDX.Mathematics.Interop; -using Buffer = SharpDX.Direct3D11.Buffer; -using DeviceContext = SharpDX.Direct2D1.DeviceContext; using DxgiFactory1 = SharpDX.DXGI.Factory1; -using Matrix = SharpDX.Matrix; using D3DDevice = SharpDX.Direct3D11.Device; using DxgiResource = SharpDX.DXGI.Resource; -using FeatureLevel = SharpDX.Direct3D.FeatureLevel; namespace GpuInterop.D3DDemo; diff --git a/samples/GpuInterop/GpuInterop.csproj b/samples/GpuInterop/GpuInterop.csproj index c201d9bf85..88e6d3d283 100644 --- a/samples/GpuInterop/GpuInterop.csproj +++ b/samples/GpuInterop/GpuInterop.csproj @@ -28,6 +28,7 @@ + diff --git a/samples/GpuInterop/Program.cs b/samples/GpuInterop/Program.cs index 86fd239b4c..8d7ccf4866 100644 --- a/samples/GpuInterop/Program.cs +++ b/samples/GpuInterop/Program.cs @@ -1,5 +1,5 @@ -using Avalonia; - +global using System.Reactive.Disposables; +using Avalonia; namespace GpuInterop { public class Program diff --git a/src/Avalonia.Base/Rendering/SwapchainBase.cs b/src/Avalonia.Base/Rendering/SwapchainBase.cs index ccfb704f00..5d0bba2341 100644 --- a/src/Avalonia.Base/Rendering/SwapchainBase.cs +++ b/src/Avalonia.Base/Rendering/SwapchainBase.cs @@ -1,8 +1,7 @@ using System; using System.Collections.Generic; -using System.Reactive.Disposables; using System.Threading.Tasks; -using Avalonia; +using Avalonia.Reactive; using Avalonia.Rendering.Composition; namespace Avalonia.Rendering; diff --git a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs index 81270bb642..bda2bd1568 100644 --- a/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs +++ b/src/Avalonia.OpenGL/Controls/OpenGlControlResources.cs @@ -1,10 +1,9 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Reactive.Disposables; using System.Threading.Tasks; using Avalonia.Logging; using Avalonia.Platform; +using Avalonia.Reactive; using Avalonia.Rendering.Composition; using static Avalonia.OpenGL.GlConsts; namespace Avalonia.OpenGL.Controls; From 32cfbe6578f2d4952eed49ddd3d6074d3f34895f Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 20 Jan 2023 15:30:27 +0800 Subject: [PATCH 19/23] fix: fix screen display in control catalog. --- samples/ControlCatalog/Pages/ScreenPage.cs | 82 +++++++++++++++++----- 1 file changed, 63 insertions(+), 19 deletions(-) diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index b5b80fb147..94fc4da7ef 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -1,5 +1,7 @@ using System; using System.Globalization; +using System.Linq; +using System.Net.Http.Headers; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -12,6 +14,11 @@ namespace ControlCatalog.Pages public class ScreenPage : UserControl { private double _leftMost; + private double _topMost; + private IBrush _primaryBrush = SolidColorBrush.Parse("#FF0078D7"); + private IBrush _defaultBrush = Brushes.LightGray; + private IPen _activePen = new Pen(Brushes.Black); + private IPen _defaultPen = new Pen(Brushes.DarkGray); protected override bool BypassFlowDirectionPolicies => true; @@ -37,51 +44,88 @@ namespace ControlCatalog.Pages var drawBrush = Brushes.Black; Pen p = new Pen(drawBrush); - foreach (Screen screen in screens) + var activeScreen = w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))); + double maxBottom = 0; + + for (int i = 0; i Date: Fri, 20 Jan 2023 15:47:57 +0800 Subject: [PATCH 20/23] fix: remove unused pen. --- samples/ControlCatalog/Pages/ScreenPage.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/samples/ControlCatalog/Pages/ScreenPage.cs b/samples/ControlCatalog/Pages/ScreenPage.cs index 94fc4da7ef..2cdd031693 100644 --- a/samples/ControlCatalog/Pages/ScreenPage.cs +++ b/samples/ControlCatalog/Pages/ScreenPage.cs @@ -41,9 +41,6 @@ namespace ControlCatalog.Pages var screens = w.Screens.All; var scaling = ((IRenderRoot)w).RenderScaling; - var drawBrush = Brushes.Black; - Pen p = new Pen(drawBrush); - var activeScreen = w.Screens.ScreenFromBounds(new PixelRect(w.Position, PixelSize.FromSize(w.Bounds.Size, scaling))); double maxBottom = 0; @@ -114,7 +111,7 @@ namespace ControlCatalog.Pages } - context.DrawRectangle(p, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f+Math.Abs(_topMost), w.Bounds.Width / 10, w.Bounds.Height / 10)); + context.DrawRectangle(_activePen, new Rect(w.Position.X / 10f + Math.Abs(_leftMost), w.Position.Y / 10f+Math.Abs(_topMost), w.Bounds.Width / 10, w.Bounds.Height / 10)); } private static FormattedText CreateFormattedText(string textToFormat, double size = 12) From 7ae394d247906b93b1347616d55bee4ac5df5890 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Fri, 20 Jan 2023 13:00:39 +0100 Subject: [PATCH 21/23] Fixed text layout buffer reset for Stack and Dictionary (+ tests) --- .../TextFormatting/FormattingBufferHelper.cs | 34 +++- .../Media/TextFormatting/Unicode/BiDiData.cs | 4 + .../FormattingBufferHelperTests.cs | 151 ++++++++++++++++++ 3 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs index 0341842cb6..c27903cd55 100644 --- a/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs +++ b/src/Avalonia.Base/Media/TextFormatting/FormattingBufferHelper.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Numerics; using System.Runtime.CompilerServices; using Avalonia.Utilities; @@ -13,7 +14,7 @@ namespace Avalonia.Media.TextFormatting { arrayBuilder.Clear(); - if (IsBufferTooLarge(arrayBuilder.Capacity)) + if (IsBufferTooLarge((uint) arrayBuilder.Capacity)) { arrayBuilder = default; } @@ -23,7 +24,7 @@ namespace Avalonia.Media.TextFormatting { list.Clear(); - if (IsBufferTooLarge(list.Capacity)) + if (IsBufferTooLarge((uint) list.Capacity)) { list.TrimExcess(); } @@ -31,9 +32,11 @@ namespace Avalonia.Media.TextFormatting public static void ClearThenResetIfTooLarge(Stack stack) { + var approximateCapacity = RoundUpToPowerOf2((uint)stack.Count); + stack.Clear(); - if (IsBufferTooLarge(stack.Count)) + if (IsBufferTooLarge(approximateCapacity)) { stack.TrimExcess(); } @@ -42,10 +45,12 @@ namespace Avalonia.Media.TextFormatting public static void ClearThenResetIfTooLarge(ref Dictionary dictionary) where TKey : notnull { + var approximateCapacity = RoundUpToPowerOf2((uint)dictionary.Count); + dictionary.Clear(); // dictionary is in fact larger than that: it has entries and buckets, but let's only count our data here - if (IsBufferTooLarge>(dictionary.Count)) + if (IsBufferTooLarge>(approximateCapacity)) { #if NET6_0_OR_GREATER dictionary.TrimExcess(); @@ -56,7 +61,24 @@ namespace Avalonia.Media.TextFormatting } [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static bool IsBufferTooLarge(int length) - => (long)Unsafe.SizeOf() * length > MaxKeptBufferSizeInBytes; + private static bool IsBufferTooLarge(uint capacity) + => (long) (uint) Unsafe.SizeOf() * capacity > MaxKeptBufferSizeInBytes; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint RoundUpToPowerOf2(uint value) + { +#if NET6_0_OR_GREATER + return BitOperations.RoundUpToPowerOf2(value); +#else + // Based on https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + --value; + value |= value >> 1; + value |= value >> 2; + value |= value >> 4; + value |= value >> 8; + value |= value >> 16; + return value + 1; +#endif + } } } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs index 8bd2171a41..5cc222b813 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiData.cs @@ -150,6 +150,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void SaveTypes() { + _hasCleanState = false; + // Capture the types data _savedClasses.Clear(); _savedClasses.Add(_classes.AsSlice()); @@ -162,6 +164,8 @@ namespace Avalonia.Media.TextFormatting.Unicode /// public void RestoreTypes() { + _hasCleanState = false; + _classes.Clear(); _classes.Add(_savedClasses.AsSlice()); _pairedBracketTypes.Clear(); diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs new file mode 100644 index 0000000000..192f34eea7 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/FormattingBufferHelperTests.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media.TextFormatting +{ + public class FormattingBufferHelperTests + { + public static TheoryData SmallSizes => new() { 1, 500, 10_000, 125_000 }; + public static TheoryData LargeSizes => new() { 500_000, 1_000_000 }; + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_List(int itemCount) + { + var capacity = FillAndClearList(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_List(int itemCount) + { + var capacity = FillAndClearList(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearList(int itemCount) + { + var list = new List(); + + for (var i = 0; i < itemCount; ++i) + { + list.Add(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(list); + + return list.Capacity; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_ArrayBuilder(int itemCount) + { + var capacity = FillAndClearArrayBuilder(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_ArrayBuilder(int itemCount) + { + var capacity = FillAndClearArrayBuilder(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearArrayBuilder(int itemCount) + { + var arrayBuilder = new ArrayBuilder(); + + for (var i = 0; i < itemCount; ++i) + { + arrayBuilder.AddItem(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref arrayBuilder); + + return arrayBuilder.Capacity; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_Stack(int itemCount) + { + var capacity = FillAndClearStack(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_Stack(int itemCount) + { + var capacity = FillAndClearStack(itemCount); + + Assert.Equal(0, capacity); + } + + private static int FillAndClearStack(int itemCount) + { + var stack = new Stack(); + + for (var i = 0; i < itemCount; ++i) + { + stack.Push(i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(stack); + + var array = (Array) stack.GetType() + .GetField("_array", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(stack)!; + + return array.Length; + } + + [Theory] + [MemberData(nameof(SmallSizes))] + public void Should_Keep_Small_Buffer_Dictionary(int itemCount) + { + var capacity = FillAndClearDictionary(itemCount); + + Assert.True(capacity >= itemCount); + } + + [Theory] + [MemberData(nameof(LargeSizes))] + public void Should_Reset_Large_Buffer_Dictionary(int itemCount) + { + var capacity = FillAndClearDictionary(itemCount); + + Assert.True(capacity <= 3); // dictionary trims to the nearest prime starting with 3 + } + + private static int FillAndClearDictionary(int itemCount) + { + var dictionary = new Dictionary(); + + for (var i = 0; i < itemCount; ++i) + { + dictionary.Add(i, i); + } + + FormattingBufferHelper.ClearThenResetIfTooLarge(ref dictionary); + + var array = (Array) dictionary.GetType() + .GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance)! + .GetValue(dictionary)!; + + return array.Length; + } + } +} From b0ebd5e1d1d35ded49d2a12c7f89d39175590cf9 Mon Sep 17 00:00:00 2001 From: cristinathoughtpennies Date: Fri, 20 Jan 2023 09:41:10 -0600 Subject: [PATCH 22/23] Update HeaderDoubleTapped functions to match conventions --- src/Avalonia.Controls/TreeViewItem.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index 532d0e32db..022e1a74b1 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -265,7 +265,10 @@ namespace Avalonia.Controls } } - protected virtual void HeaderDoubleTapped(object? sender, TappedEventArgs e) + /// + /// Invoked when the event occurs in the header. + /// + protected virtual void OnHeaderDoubleTapped(TappedEventArgs e) { if (ItemCount > 0) { @@ -286,5 +289,10 @@ namespace Avalonia.Controls return logical != null ? result : @default; } + + private void HeaderDoubleTapped(object? sender, TappedEventArgs e) + { + OnHeaderDoubleTapped(e); + } } } From 07c11c75e7b216a66ba2c965dbc1fdf4ccb13717 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 20 Jan 2023 20:28:54 +0100 Subject: [PATCH 23/23] Use IGlyphRunImpl in the IDrawingContextImpl --- src/Avalonia.Base/Media/DrawingContext.cs | 2 +- src/Avalonia.Base/Media/DrawingGroup.cs | 5 +- src/Avalonia.Base/Media/GlyphRun.cs | 55 ++++++++----------- src/Avalonia.Base/Media/GlyphRunMetrics.cs | 15 ++--- .../Media/ImmediateDrawingContext.cs | 2 +- src/Avalonia.Base/Media/TextDecoration.cs | 2 +- .../Media/TextFormatting/TextLineImpl.cs | 2 +- .../Platform/IDrawingContextImpl.cs | 2 +- src/Avalonia.Base/Platform/IGlyphRunImpl.cs | 19 ++++++- .../Drawing/CompositionDrawingContext.cs | 2 +- .../Composition/Server/DrawingContextProxy.cs | 2 +- .../Composition/Server/FpsCounter.cs | 2 +- .../SceneGraph/DeferredDrawingContextImpl.cs | 2 +- .../Rendering/SceneGraph/GlyphRunNode.cs | 18 ++++-- .../HeadlessPlatformRenderInterface.cs | 8 ++- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 10 ++-- src/Skia/Avalonia.Skia/GlyphRunImpl.cs | 10 +++- .../Avalonia.Skia/PlatformRenderInterface.cs | 14 +++-- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 14 ++++- .../Media/DrawingContextImpl.cs | 8 +-- .../Avalonia.Direct2D1/Media/GlyphRunImpl.cs | 12 +++- .../Media/GlyphRunTests.cs | 4 +- .../NullDrawingContextImpl.cs | 2 +- tests/Avalonia.Benchmarks/NullGlyphRun.cs | 4 ++ .../NullRenderingPlatform.cs | 2 +- .../DatePickerTests.cs | 3 +- .../MaskedTextBoxTests.cs | 8 +-- .../Primitives/SelectingItemsControlTests.cs | 4 +- .../TimePickerTests.cs | 3 +- .../Media/GlyphRunTests.cs | 4 +- tests/Avalonia.UnitTests/MockGlyphRun.cs | 11 ++++ .../MockPlatformRenderInterface.cs | 2 +- 32 files changed, 154 insertions(+), 99 deletions(-) diff --git a/src/Avalonia.Base/Media/DrawingContext.cs b/src/Avalonia.Base/Media/DrawingContext.cs index 077816c645..d295111d72 100644 --- a/src/Avalonia.Base/Media/DrawingContext.cs +++ b/src/Avalonia.Base/Media/DrawingContext.cs @@ -246,7 +246,7 @@ namespace Avalonia.Media if (foreground != null) { - PlatformImpl.DrawGlyphRun(foreground, glyphRun); + PlatformImpl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); } } diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 7d3b4c056e..481329c20c 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -167,18 +167,17 @@ namespace Avalonia.Media AddNewGeometryDrawing(brush, pen, new PlatformGeometry(geometry)); } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { if (foreground == null || glyphRun == null) { return; } - // Add a GlyphRunDrawing to the Drawing graph GlyphRunDrawing glyphRunDrawing = new GlyphRunDrawing { Foreground = foreground, - GlyphRun = glyphRun, + GlyphRun = new GlyphRun(glyphRun) }; // Add Drawing to the Drawing graph diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index b637c94d88..65575617d0 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -13,10 +13,9 @@ namespace Avalonia.Media /// public sealed class GlyphRun : IDisposable { - private IGlyphRunImpl? _glyphRunImpl; + private IRef? _platformImpl; private double _fontRenderingEmSize; private int _biDiLevel; - private Point? _baselineOrigin; private GlyphRunMetrics? _glyphRunMetrics; private ReadOnlyMemory _characters; private IReadOnlyList _glyphInfos; @@ -68,6 +67,13 @@ namespace Avalonia.Media _biDiLevel = biDiLevel; } + internal GlyphRun(IRef platformImpl) + { + _glyphInfos = Array.Empty(); + GlyphTypeface = Typeface.Default.GlyphTypeface; + _platformImpl = platformImpl; + } + private static IReadOnlyList CreateGlyphInfos(IReadOnlyList glyphIndices, double fontRenderingEmSize, IGlyphTypeface glyphTypeface) { @@ -132,7 +138,7 @@ namespace Avalonia.Media /// /// Gets or sets the conservative bounding box of the . /// - public Size Size => new Size(Metrics.WidthIncludingTrailingWhitespace, Metrics.Height); + public Size Size => PlatformImpl.Item.Size; /// /// @@ -141,13 +147,9 @@ namespace Avalonia.Media => _glyphRunMetrics ??= CreateGlyphRunMetrics(); /// - /// Gets or sets the baseline origin of the. + /// Gets the baseline origin of the. /// - public Point BaselineOrigin - { - get => _baselineOrigin ??= CalculateBaselineOrigin(); - set => Set(ref _baselineOrigin, value); - } + public Point BaselineOrigin => PlatformImpl.Item.BaselineOrigin; /// /// Gets or sets the list of UTF16 code points that represent the Unicode content of the . @@ -193,8 +195,8 @@ namespace Avalonia.Media /// /// The platform implementation of the . /// - public IGlyphRunImpl GlyphRunImpl - => _glyphRunImpl ??= CreateGlyphRunImpl(); + public IRef PlatformImpl + => _platformImpl ??= CreateGlyphRunImpl(); /// /// Obtains geometry for the glyph run. @@ -233,7 +235,7 @@ namespace Avalonia.Media if (characterIndex > Metrics.LastCluster) { - return Metrics.WidthIncludingTrailingWhitespace; + return Size.Width; } var glyphIndex = FindGlyphIndex(characterIndex); @@ -607,15 +609,6 @@ namespace Avalonia.Media return new CharacterHit(cluster, clusterLength); } - /// - /// Calculates the default baseline origin of the . - /// - /// The baseline origin. - private Point CalculateBaselineOrigin() - { - return new Point(0, -GlyphTypeface.Metrics.Ascent * Scale); - } - private GlyphRunMetrics CreateGlyphRunMetrics() { int firstCluster, lastCluster; @@ -668,8 +661,6 @@ namespace Avalonia.Media return new GlyphRunMetrics( width, - widthIncludingTrailingWhitespace, - height, trailingWhitespaceLength, newLineLength, firstCluster, @@ -800,28 +791,30 @@ namespace Avalonia.Media private void Set(ref T field, T value) { - _glyphRunImpl?.Dispose(); + _platformImpl?.Dispose(); - _glyphRunImpl = null; + _platformImpl = null; _glyphRunMetrics = null; - _baselineOrigin = null; - field = value; } - private IGlyphRunImpl CreateGlyphRunImpl() + private IRef CreateGlyphRunImpl() { var platformRenderInterface = AvaloniaLocator.Current.GetRequiredService(); - return platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos); + var platformImpl = platformRenderInterface.CreateGlyphRun(GlyphTypeface, FontRenderingEmSize, GlyphInfos); + + _platformImpl = RefCountable.Create(platformImpl); + + return _platformImpl; } public void Dispose() { - _glyphRunImpl?.Dispose(); - _glyphRunImpl = null; + _platformImpl?.Dispose(); + _platformImpl = null; } } } diff --git a/src/Avalonia.Base/Media/GlyphRunMetrics.cs b/src/Avalonia.Base/Media/GlyphRunMetrics.cs index 492b5214cd..09b183d044 100644 --- a/src/Avalonia.Base/Media/GlyphRunMetrics.cs +++ b/src/Avalonia.Base/Media/GlyphRunMetrics.cs @@ -2,27 +2,20 @@ { public readonly record struct GlyphRunMetrics { - public GlyphRunMetrics(double width, double widthIncludingTrailingWhitespace, double height, - int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster) + public GlyphRunMetrics(double width, int trailingWhitespaceLength, int newLineLength, int firstCluster, int lastCluster) { Width = width; - WidthIncludingTrailingWhitespace = widthIncludingTrailingWhitespace; - Height = height; TrailingWhitespaceLength = trailingWhitespaceLength; - NewLineLength= newLineLength; + NewLineLength = newLineLength; FirstCluster = firstCluster; LastCluster = lastCluster; } public double Width { get; } - public double WidthIncludingTrailingWhitespace { get; } - - public double Height { get; } - public int TrailingWhitespaceLength { get; } - - public int NewLineLength { get; } + + public int NewLineLength { get; } public int FirstCluster { get; } diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index eb6f105680..7d9534c414 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -182,7 +182,7 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - public void DrawGlyphRun(IImmutableBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IImmutableBrush foreground, IRef glyphRun) { _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); diff --git a/src/Avalonia.Base/Media/TextDecoration.cs b/src/Avalonia.Base/Media/TextDecoration.cs index d6b2841214..dc9e5cb907 100644 --- a/src/Avalonia.Base/Media/TextDecoration.cs +++ b/src/Avalonia.Base/Media/TextDecoration.cs @@ -218,7 +218,7 @@ namespace Avalonia.Media { var offsetY = glyphRun.BaselineOrigin.Y - origin.Y; - var intersections = glyphRun.GlyphRunImpl.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); + var intersections = glyphRun.PlatformImpl.Item.GetIntersections((float)(thickness * 0.5d - offsetY), (float)(thickness * 1.5d - offsetY)); if (intersections != null && intersections.Count > 0) { diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 245104f8fe..260fcaccbe 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -1320,7 +1320,7 @@ namespace Avalonia.Media.TextFormatting newLineLength = textRun.GlyphRun.Metrics.NewLineLength; } - widthIncludingWhitespace += textRun.GlyphRun.Metrics.WidthIncludingTrailingWhitespace; + widthIncludingWhitespace += textRun.Size.Width; break; } diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 6aa5eeea3d..c05c04c22e 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -91,7 +91,7 @@ namespace Avalonia.Platform /// /// The foreground. /// The glyph run. - void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun); + void DrawGlyphRun(IBrush foreground, IRef glyphRun); /// /// Creates a new that can be used as a render layer diff --git a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs index 6a8ae4d954..46b065b04e 100644 --- a/src/Avalonia.Base/Platform/IGlyphRunImpl.cs +++ b/src/Avalonia.Base/Platform/IGlyphRunImpl.cs @@ -10,6 +10,23 @@ namespace Avalonia.Platform [Unstable] public interface IGlyphRunImpl : IDisposable { - IReadOnlyList GetIntersections(float lowerBound, float upperBound); + + /// + /// Gets the conservative bounding box of the glyph run./>. + /// + Size Size { get; } + + /// + /// Gets the baseline origin of the glyph run./>. + /// + Point BaselineOrigin { get; } + + /// + /// Gets the intersections of specified upper and lower limit. + /// + /// Upper limit. + /// Lower limit. + /// + IReadOnlyList GetIntersections(float lowerLimit, float upperLimit); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs index aae1fcb90e..05488a558f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs @@ -159,7 +159,7 @@ internal class CompositionDrawingContext : IDrawingContextImpl, IDrawingContextW public object? GetFeature(Type t) => null; /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { var next = NextDrawAs(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index e6bbba6ec0..c58beebe7f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -86,7 +86,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, IDrawingCont _impl.DrawEllipse(brush, pen, rect); } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { _impl.DrawGlyphRun(foreground, glyphRun); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs index 32923a5257..ebab39cee8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/FpsCounter.cs @@ -72,7 +72,7 @@ internal class FpsCounter { var run = _runs[ch - FirstChar]; context.Transform = Matrix.CreateTranslation(offset, 0); - context.DrawGlyphRun(Brushes.White, run); + context.DrawGlyphRun(Brushes.White, run.PlatformImpl); offset += run.Size.Width; } } diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index d6766fa9b8..b1d8301557 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -206,7 +206,7 @@ namespace Avalonia.Rendering.SceneGraph public object? GetFeature(Type t) => null; /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { var next = NextDrawAs(); diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs index 1d85e95835..a2d914bdd7 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Rendering.SceneGraph { @@ -19,13 +20,13 @@ namespace Avalonia.Rendering.SceneGraph public GlyphRunNode( Matrix transform, IBrush foreground, - GlyphRun glyphRun, + IRef glyphRun, IDisposable? aux = null) - : base(new Rect(glyphRun.Size), transform, aux) + : base(new Rect(glyphRun.Item.Size), transform, aux) { Transform = transform; Foreground = foreground.ToImmutable(); - GlyphRun = glyphRun; + GlyphRun = glyphRun.Clone(); } /// @@ -41,7 +42,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// Gets the glyph run to draw. /// - public GlyphRun GlyphRun { get; } + public IRef GlyphRun { get; } /// public override void Render(IDrawingContextImpl context) @@ -61,14 +62,19 @@ namespace Avalonia.Rendering.SceneGraph /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - internal bool Equals(Matrix transform, IBrush foreground, GlyphRun glyphRun) + internal bool Equals(Matrix transform, IBrush foreground, IRef glyphRun) { return transform == Transform && Equals(foreground, Foreground) && - Equals(glyphRun, GlyphRun); + Equals(glyphRun.Item, GlyphRun.Item); } /// public override bool HitTest(Point p) => Bounds.ContainsExclusive(p); + + public override void Dispose() + { + GlyphRun?.Dispose(); + } } } diff --git a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index e368ddc373..739a648bac 100644 --- a/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -126,13 +126,17 @@ namespace Avalonia.Headless class HeadlessGlyphRunStub : IGlyphRunImpl { + public Size Size => new Size(8, 12); + + public Point BaselineOrigin => new Point(0, 8); + public void Dispose() { } public IReadOnlyList GetIntersections(float lowerBound, float upperBound) { - throw new NotImplementedException(); + return null; } } @@ -463,7 +467,7 @@ namespace Avalonia.Headless { } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { } diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 8b71f4e17e..dcb20d2a44 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -492,15 +492,15 @@ namespace Avalonia.Skia } /// - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { CheckLease(); - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Size)) { - var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; + var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; - Canvas.DrawText(glyphRunImpl.TextBlob, (float)glyphRun.BaselineOrigin.X, - (float)glyphRun.BaselineOrigin.Y, paintWrapper.Paint); + Canvas.DrawText(glyphRunImpl.TextBlob, (float)glyphRun.Item.BaselineOrigin.X, + (float)glyphRun.Item.BaselineOrigin.Y, paintWrapper.Paint); } } diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index dd7ed31a6e..cc669f9aaa 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -11,9 +11,13 @@ namespace Avalonia.Skia [Unstable] public class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SKTextBlob textBlob) + public GlyphRunImpl(SKTextBlob textBlob, Size size, Point baselineOrigin) { TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob)); + + Size = size; + + BaselineOrigin = baselineOrigin; } /// @@ -21,6 +25,10 @@ namespace Avalonia.Skia /// public SKTextBlob TextBlob { get; } + public Size Size { get; } + + public Point BaselineOrigin { get; } + public IReadOnlyList GetIntersections(float upperBound, float lowerBound) => TextBlob.GetIntercepts(lowerBound, upperBound); diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 3fb7491898..6630f0707e 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -81,7 +81,7 @@ namespace Avalonia.Skia SKPath path = new SKPath(); - var (currentX, currentY) = glyphRun.BaselineOrigin; + var (currentX, currentY) = glyphRun.PlatformImpl.Item.BaselineOrigin; for (var i = 0; i < glyphRun.GlyphInfos.Count; i++) { @@ -236,7 +236,7 @@ namespace Avalonia.Skia var glyphSpan = runBuffer.GetGlyphSpan(); var positionSpan = runBuffer.GetPositionSpan(); - var currentX = 0.0; + var width = 0.0; for (int i = 0; i < count; i++) { @@ -245,12 +245,16 @@ namespace Avalonia.Skia glyphSpan[i] = glyphInfo.GlyphIndex; - positionSpan[i] = new SKPoint((float)(currentX + offset.X), (float)offset.Y); + positionSpan[i] = new SKPoint((float)(width + offset.X), (float)offset.Y); - currentX += glyphInfo.GlyphAdvance; + width += glyphInfo.GlyphAdvance; } - return new GlyphRunImpl(builder.Build()); + var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; + var height = glyphTypeface.Metrics.LineSpacing * scale; + var baselineOrigin = new Point(0, -glyphTypeface.Metrics.Ascent * scale); + + return new GlyphRunImpl(builder.Build(), new Size(width, height), baselineOrigin); } } } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index d9cd0590fc..a2f99d9d71 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -181,9 +181,15 @@ namespace Avalonia.Direct2D1 run.Advances = new float[glyphCount]; + var width = 0.0; + for (var i = 0; i < glyphCount; i++) { - run.Advances[i] = (float)glyphInfos[i].GlyphAdvance; + var advance = glyphInfos[i].GlyphAdvance; + + width += advance; + + run.Advances[i] = (float)advance; } run.Offsets = new GlyphOffset[glyphCount]; @@ -199,7 +205,11 @@ namespace Avalonia.Direct2D1 }; } - return new GlyphRunImpl(run); + var scale = fontRenderingEmSize / glyphTypeface.Metrics.DesignEmHeight; + var height = glyphTypeface.Metrics.LineSpacing * scale; + var baselineOrigin = new Point(0, -glyphTypeface.Metrics.Ascent * scale); + + return new GlyphRunImpl(run, new Size(width, height), baselineOrigin); } class D2DApi : IPlatformRenderInterfaceContext diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 3f2298eb22..d5d6cd8c29 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -390,13 +390,13 @@ namespace Avalonia.Direct2D1.Media /// /// The foreground. /// The glyph run. - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Item.Size)) { - var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl; + var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; - _renderTarget.DrawGlyphRun(glyphRun.BaselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, + _renderTarget.DrawGlyphRun(glyphRun.Item.BaselineOrigin.ToSharpDX(), glyphRunImpl.GlyphRun, brush.PlatformBrush, MeasuringMode.Natural); } } diff --git a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs index 67418613a4..24b8fc04b3 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/GlyphRunImpl.cs @@ -1,20 +1,26 @@ using System.Collections.Generic; using Avalonia.Platform; +using SharpDX.DirectWrite; namespace Avalonia.Direct2D1.Media { internal class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SharpDX.DirectWrite.GlyphRun glyphRun) + public GlyphRunImpl(GlyphRun glyphRun, Size size, Point baselineOrigin) { + Size = size; + BaselineOrigin = baselineOrigin; GlyphRun = glyphRun; } - public SharpDX.DirectWrite.GlyphRun GlyphRun { get; } + public Size Size { get; } + + public Point BaselineOrigin { get; } + + public GlyphRun GlyphRun { get; } public void Dispose() { - //SharpDX already handles this. //GlyphRun?.Dispose(); } diff --git a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs index 3573ba6b07..a05bfbea4c 100644 --- a/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/GlyphRunTests.cs @@ -25,7 +25,7 @@ namespace Avalonia.Base.UnitTests.Media [Theory] public void Should_Get_Distance_From_CharacterHit(double[] advances, int[] clusters, int start, int trailingLength, double expectedDistance) { - using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using(UnitTestApplication.Start(TestServices.StyledWindow)) using (var glyphRun = CreateGlyphRun(advances, clusters)) { var characterHit = new CharacterHit(start, trailingLength); @@ -44,7 +44,7 @@ namespace Avalonia.Base.UnitTests.Media public void Should_Get_CharacterHit_FromDistance(double[] advances, int[] clusters, double distance, int start, int trailingLengthExpected, bool isInsideExpected) { - using(UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + using(UnitTestApplication.Start(TestServices.StyledWindow)) using (var glyphRun = CreateGlyphRun(advances, clusters)) { var textBounds = glyphRun.GetCharacterHitFromDistance(distance, out var isInside); diff --git a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs index 8436881122..e83b2d7598 100644 --- a/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs +++ b/tests/Avalonia.Benchmarks/NullDrawingContextImpl.cs @@ -44,7 +44,7 @@ namespace Avalonia.Benchmarks { } - public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun) + public void DrawGlyphRun(IBrush foreground, IRef glyphRun) { } diff --git a/tests/Avalonia.Benchmarks/NullGlyphRun.cs b/tests/Avalonia.Benchmarks/NullGlyphRun.cs index 1c2c7c0d7d..c4707c78c8 100644 --- a/tests/Avalonia.Benchmarks/NullGlyphRun.cs +++ b/tests/Avalonia.Benchmarks/NullGlyphRun.cs @@ -5,6 +5,10 @@ namespace Avalonia.Benchmarks { internal class NullGlyphRun : IGlyphRunImpl { + public Size Size => default; + + public Point BaselineOrigin => default; + public void Dispose() { } diff --git a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs index a802cd0958..7a17efa59b 100644 --- a/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs +++ b/tests/Avalonia.Benchmarks/NullRenderingPlatform.cs @@ -123,7 +123,7 @@ namespace Avalonia.Benchmarks public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { - return new MockGlyphRun(); + return new MockGlyphRun(glyphInfos); } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index 894dc5b996..3b2ca51976 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -205,7 +205,8 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new MockFontManagerImpl(), standardCursorFactory: Mock.Of(), - textShaperImpl: new MockTextShaperImpl()); + textShaperImpl: new MockTextShaperImpl(), + renderInterface: new MockPlatformRenderInterface()); private static IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs index c1d9fad6f4..2732f6a5fb 100644 --- a/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/MaskedTextBoxTests.cs @@ -107,7 +107,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void CaretIndex_Can_Moved_To_Position_After_The_End_Of_Text_With_Arrow_Key() { - using (Start()) + using (Start(TestServices.StyledWindow)) { var target = new MaskedTextBox { @@ -182,7 +182,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Control_Backspace_Should_Remove_The_Word_Before_The_Caret_If_There_Is_No_Selection() { - using (Start()) + using (Start(TestServices.StyledWindow)) { MaskedTextBox textBox = new MaskedTextBox { @@ -224,7 +224,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Control_Delete_Should_Remove_The_Word_After_The_Caret_If_There_Is_No_Selection() { - using (Start()) + using (Start(TestServices.StyledWindow)) { var textBox = new MaskedTextBox { @@ -810,7 +810,7 @@ namespace Avalonia.Controls.UnitTests bool fromClipboard, string expected) { - using (Start()) + using (Start(TestServices.StyledWindow)) { var target = new MaskedTextBox { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 1775d7ef70..7d129d987e 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -2131,9 +2131,7 @@ namespace Avalonia.Controls.UnitTests.Primitives private static IDisposable Start() { - return UnitTestApplication.Start(new TestServices( - fontManagerImpl: new MockFontManagerImpl(), - textShaperImpl: new MockTextShaperImpl())); + return UnitTestApplication.Start(TestServices.StyledWindow); } private static void Prepare(SelectingItemsControl target) diff --git a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs index 2fd10010a6..e0bf230d99 100644 --- a/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TimePickerTests.cs @@ -101,7 +101,8 @@ namespace Avalonia.Controls.UnitTests private static TestServices Services => TestServices.MockThreadingInterface.With( fontManagerImpl: new MockFontManagerImpl(), standardCursorFactory: Mock.Of(), - textShaperImpl: new MockTextShaperImpl()); + textShaperImpl: new MockTextShaperImpl(), + renderInterface: new MockPlatformRenderInterface()); private static IControlTemplate CreateTemplate() { diff --git a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs index f2d6670be5..59c7ac3786 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/GlyphRunTests.cs @@ -110,7 +110,7 @@ namespace Avalonia.Skia.UnitTests.Media if (glyphRun.IsLeftToRight) { var characterHit = - glyphRun.GetCharacterHitFromDistance(glyphRun.Metrics.WidthIncludingTrailingWhitespace, out _); + glyphRun.GetCharacterHitFromDistance(glyphRun.Size.Width, out _); Assert.Equal(glyphRun.Characters.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength); } @@ -159,7 +159,7 @@ namespace Avalonia.Skia.UnitTests.Media { var height = glyphRun.Size.Height; - var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Metrics.WidthIncludingTrailingWhitespace; + var currentX = glyphRun.IsLeftToRight ? 0d : glyphRun.Size.Width; var rects = new List(glyphRun.GlyphInfos!.Count); diff --git a/tests/Avalonia.UnitTests/MockGlyphRun.cs b/tests/Avalonia.UnitTests/MockGlyphRun.cs index f525e4736b..45e7b47f62 100644 --- a/tests/Avalonia.UnitTests/MockGlyphRun.cs +++ b/tests/Avalonia.UnitTests/MockGlyphRun.cs @@ -1,10 +1,21 @@ using System.Collections.Generic; +using System.Linq; +using Avalonia.Media.TextFormatting; using Avalonia.Platform; namespace Avalonia.UnitTests { public class MockGlyphRun : IGlyphRunImpl { + public MockGlyphRun(IReadOnlyList glyphInfos) + { + Size = new Size(glyphInfos.Sum(x=> x.GlyphAdvance), 10); + } + + public Size Size { get; } + + public Point BaselineOrigin => new Point(0, 8); + public void Dispose() { diff --git a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs index d56e360e22..2a00ef8e40 100644 --- a/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs +++ b/tests/Avalonia.UnitTests/MockPlatformRenderInterface.cs @@ -149,7 +149,7 @@ namespace Avalonia.UnitTests public IGlyphRunImpl CreateGlyphRun(IGlyphTypeface glyphTypeface, double fontRenderingEmSize, IReadOnlyList glyphInfos) { - return Mock.Of(); + return new MockGlyphRun(glyphInfos); } public IPlatformRenderInterfaceContext CreateBackendContext(IPlatformGraphicsContext graphicsContext) => this;