From 24f208bab1fce4a7ed9403703a0b7f9ff07d2c2b Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 25 Mar 2019 17:45:42 +0100 Subject: [PATCH 1/9] !R Rework deferred setter for better performance. --- src/Avalonia.Base/AvaloniaObject.cs | 91 +++++++++-- .../Utilities/AvaloniaPropertyCollection.cs | 143 ++++++++++++++++ .../Utilities/DeferredSetterOptimized.cs | 84 ++++++++++ src/Avalonia.Base/ValueStore.cs | 153 ++++-------------- .../Base/DirectPropertyBenchmark.cs | 82 ++++++++++ tests/Avalonia.Benchmarks/Base/Properties.cs | 3 +- 6 files changed, 420 insertions(+), 136 deletions(-) create mode 100644 src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs create mode 100644 src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs create mode 100644 tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7601b64ce9..22aeccc3fb 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -561,6 +561,7 @@ namespace Avalonia protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) { VerifyAccess(); + return SetAndRaise( property, ref field, @@ -568,19 +569,89 @@ namespace Avalonia => SetAndRaiseCore(property, ref backing, val, notifyWrapper), value); } + + /// + /// Default setter handler that will set backing field and raise notification. + /// + private sealed class DefaultSetterHandler : DeferredSetterOptimized.ISetterHandler + { + public static readonly DefaultSetterHandler Instance = new DefaultSetterHandler(); + + public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) + { + var old = backing; + backing = value; + + source.RaisePropertyChanged(property, old, value); + + return true; + } + } /// - /// Default assignment logic for SetAndRaise. + /// Setter handler that will run custom user callback. /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) + private sealed class CallbackSetterHandler : DeferredSetterOptimized.ISetterHandler + { + private readonly SetAndRaiseCallback _callback; + + public CallbackSetterHandler(SetAndRaiseCallback callback) + { + _callback = callback; + } + + public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) + { + _callback(value, ref backing, notification => notification()); + + return true; + } + } + + protected bool SetAndRaiseOptimized(AvaloniaProperty property, ref T field, T value) + { + VerifyAccess(); + + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + + return setter.SetAndNotify(this, property, DefaultSetterHandler.Instance, ref field, value); + } + + protected bool SetAndRaiseOptimized( + AvaloniaProperty property, + ref T field, + SetAndRaiseCallback setterCallback, + T value) + { + VerifyAccess(); + + if (EqualityComparer.Default.Equals(field, value)) + { + return false; + } + + DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + + return setter.SetAndNotify(this, property, new CallbackSetterHandler(setterCallback) , ref field, value); + } + + /// + /// Default assignment logic for SetAndRaise. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// The value. + /// A wrapper for the property-changed notification. + /// + /// True if the value changed, otherwise false. + /// + private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) { var old = field; field = value; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs new file mode 100644 index 0000000000..5b1492a7a8 --- /dev/null +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.Generic; + +namespace Avalonia.Utilities +{ + internal sealed class AvaloniaPropertyCollection + { + private Entry[] _entries; + + public AvaloniaPropertyCollection() + { + // The last item in the list is always int.MaxValue + _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; + } + + private (int, bool) TryFindEntry(int propertyId) + { + if (_entries.Length <= 12) + { + // For small lists, we use an optimized linear search. Since the last item in the list + // is always int.MaxValue, we can skip a conditional branch in each iteration. + // By unrolling the loop, we can skip another unconditional branch in each iteration. + + if (_entries[0].PropertyId >= propertyId) + return (0, _entries[0].PropertyId == propertyId); + if (_entries[1].PropertyId >= propertyId) + return (1, _entries[1].PropertyId == propertyId); + if (_entries[2].PropertyId >= propertyId) + return (2, _entries[2].PropertyId == propertyId); + if (_entries[3].PropertyId >= propertyId) + return (3, _entries[3].PropertyId == propertyId); + if (_entries[4].PropertyId >= propertyId) + return (4, _entries[4].PropertyId == propertyId); + if (_entries[5].PropertyId >= propertyId) + return (5, _entries[5].PropertyId == propertyId); + if (_entries[6].PropertyId >= propertyId) + return (6, _entries[6].PropertyId == propertyId); + if (_entries[7].PropertyId >= propertyId) + return (7, _entries[7].PropertyId == propertyId); + if (_entries[8].PropertyId >= propertyId) + return (8, _entries[8].PropertyId == propertyId); + if (_entries[9].PropertyId >= propertyId) + return (9, _entries[9].PropertyId == propertyId); + if (_entries[10].PropertyId >= propertyId) + return (10, _entries[10].PropertyId == propertyId); + } + else + { + int low = 0; + int high = _entries.Length; + int id; + + while (high - low > 3) + { + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; + + if (propertyId == id) + return (pivot, true); + + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } + + do + { + id = _entries[low].PropertyId; + + if (id == propertyId) + return (low, true); + + if (id > propertyId) + break; + + ++low; + } + while (low < high); + } + + return (0, false); + } + + public bool TryGetValue(AvaloniaProperty property, out TValue value) + { + (int index, bool found) = TryFindEntry(property.Id); + if (!found) + { + value = default; + return false; + } + + value = _entries[index].Value; + return true; + } + + public void AddValueInternal(AvaloniaProperty property, TValue value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + public void SetValueInternal(AvaloniaProperty property, TValue value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + public Dictionary ToDictionary() + { + var dict = new Dictionary(_entries.Length - 1); + + for (int i = 0; i < _entries.Length - 1; ++i) + { + dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); + } + + return dict; + } + + private struct Entry + { + internal int PropertyId; + internal TValue Value; + } + } +} diff --git a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs new file mode 100644 index 0000000000..e3bed90498 --- /dev/null +++ b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs @@ -0,0 +1,84 @@ +using System; + +namespace Avalonia.Utilities +{ + /// + /// A utility class to enable deferring assignment until after property-changed notifications are sent. + /// Used to fix #855. + /// + /// The type of value with which to track the delayed assignment. + internal sealed class DeferredSetterOptimized + { + private bool _isNotifying; + private readonly SingleOrQueue _pendingValues; + + public DeferredSetterOptimized() + { + _pendingValues = new SingleOrQueue(); + } + + public bool SetAndNotify( + AvaloniaObject source, + AvaloniaProperty property, + ISetterHandler handler, + ref TSetRecord backing, + TSetRecord value) + { + if (!_isNotifying) + { + bool updated; + + using (new NotifyDisposable(this)) + { + updated = handler.Update(source, property, ref backing, value); + } + + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + updated = handler.Update(source, property, ref backing, _pendingValues.Dequeue()); + } + } + } + + return updated; + } + + _pendingValues.Enqueue(value); + + return false; + } + + /// + /// Disposable that marks the property as currently notifying. + /// When disposed, marks the property as done notifying. + /// + private readonly struct NotifyDisposable : IDisposable + { + private readonly DeferredSetterOptimized _setter; + + internal NotifyDisposable(DeferredSetterOptimized setter) + { + _setter = setter; + _setter._isNotifying = true; + } + + public void Dispose() + { + _setter._isNotifying = false; + } + } + + public interface ISetterHandler + { + bool Update( + AvaloniaObject source, + AvaloniaProperty property, + ref TSetRecord backing, + TSetRecord value); + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 24f85ea6b1..b2c4c68f9c 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,21 +7,15 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - private struct Entry - { - internal int PropertyId; - internal object Value; - } - + private readonly AvaloniaPropertyCollection _propertyValues; + private readonly AvaloniaPropertyCollection _deferredSetters; private readonly AvaloniaObject _owner; - private Entry[] _entries; public ValueStore(AvaloniaObject owner) { _owner = owner; - - // The last item in the list is always int.MaxValue - _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } }; + _propertyValues = new AvaloniaPropertyCollection(); + _deferredSetters = new AvaloniaPropertyCollection(); } public IDisposable AddBinding( @@ -31,7 +25,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -39,13 +33,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValueInternal(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValueInternal(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -55,7 +49,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (TryGetValue(property, out var v)) + if (_propertyValues.TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -63,7 +57,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - SetValueInternal(property, Validate(property, value)); + _propertyValues.SetValueInternal(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -71,7 +65,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - SetValueInternal(property, priorityValue); + _propertyValues.SetValueInternal(property, priorityValue); } } } @@ -84,14 +78,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - AddValueInternal(property, Validate(property, value)); + _propertyValues.AddValueInternal(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - AddValueInternal(property, priorityValue); + _propertyValues.AddValueInternal(property, priorityValue); } } @@ -110,14 +104,9 @@ namespace Avalonia public IDictionary GetSetValues() { - var dict = new Dictionary(_entries.Length - 1); - for (int i = 0; i < _entries.Length - 1; ++i) - { - dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); - } - - return dict; + return _propertyValues.ToDictionary(); } + public void LogError(AvaloniaProperty property, Exception e) { _owner.LogBindingError(property, e); @@ -127,7 +116,7 @@ namespace Avalonia { var result = AvaloniaProperty.UnsetValue; - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; } @@ -137,12 +126,12 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + return _propertyValues.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; } public bool IsSet(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; } @@ -152,7 +141,7 @@ namespace Avalonia public void Revalidate(AvaloniaProperty property) { - if (TryGetValue(property, out var value)) + if (_propertyValues.TryGetValue(property, out var value)) { (value as PriorityValue)?.Revalidate(); } @@ -189,113 +178,29 @@ namespace Avalonia return value; } - private DeferredSetter _deferredSetter; - - public DeferredSetter Setter - { - get - { - return _deferredSetter ?? - (_deferredSetter = new DeferredSetter()); - } - } - - private bool TryGetValue(AvaloniaProperty property, out object value) + public DeferredSetterOptimized GetDeferredSetter(AvaloniaProperty property) { - (int index, bool found) = TryFindEntry(property.Id); - if (!found) + if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { - value = null; - return false; + return (DeferredSetterOptimized)deferredSetter; } - value = _entries[index].Value; - return true; - } - - private void AddValueInternal(AvaloniaProperty property, object value) - { - Entry[] entries = new Entry[_entries.Length + 1]; - - for (int i = 0; i < _entries.Length; ++i) - { - if (_entries[i].PropertyId > property.Id) - { - if (i > 0) - { - Array.Copy(_entries, 0, entries, 0, i); - } + var newDeferredSetter = new DeferredSetterOptimized(); - entries[i] = new Entry { PropertyId = property.Id, Value = value }; - Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); - break; - } - } + _deferredSetters.AddValueInternal(property, newDeferredSetter); - _entries = entries; + return newDeferredSetter; } - private void SetValueInternal(AvaloniaProperty property, object value) - { - _entries[TryFindEntry(property.Id).Item1].Value = value; - } + private DeferredSetter _deferredSetter; - private (int, bool) TryFindEntry(int propertyId) + public DeferredSetter Setter { - if (_entries.Length <= 12) - { - // For small lists, we use an optimized linear search. Since the last item in the list - // is always int.MaxValue, we can skip a conditional branch in each iteration. - // By unrolling the loop, we can skip another unconditional branch in each iteration. - - if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId); - if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId); - if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId); - if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId); - if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId); - if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId); - if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId); - if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId); - if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); - if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); - if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); - } - else + get { - int low = 0; - int high = _entries.Length; - int id; - - while (high - low > 3) - { - int pivot = (high + low) / 2; - id = _entries[pivot].PropertyId; - - if (propertyId == id) - return (pivot, true); - - if (propertyId <= id) - high = pivot; - else - low = pivot + 1; - } - - do - { - id = _entries[low].PropertyId; - - if (id == propertyId) - return (low, true); - - if (id > propertyId) - break; - - ++low; - } - while (low < high); + return _deferredSetter ?? + (_deferredSetter = new DeferredSetter()); } - - return (0, false); } } } diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs new file mode 100644 index 0000000000..4f36b54414 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -0,0 +1,82 @@ +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base +{ + [MemoryDiagnoser] + public class DirectPropertyBenchmark + { + [Benchmark(Baseline = true)] + public void SetAndRaiseOriginal() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValue += 1; + } + } + + [Benchmark] + public void SetAndRaiseOptimized() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValueOptimized += 1; + } + } + + [Benchmark] + public void SetAndRaiseSimple() + { + var obj = new DirectClass(); + + for (var i = 0; i < 100; ++i) + { + obj.IntValueSimple += 1; + } + } + + class DirectClass : AvaloniaObject + { + private int _intValue; + + public static readonly DirectProperty IntValueProperty = + AvaloniaProperty.RegisterDirect(nameof(IntValue), + o => o.IntValue, + (o, v) => o.IntValue = v); + + public int IntValue + { + get => _intValue; + set => SetAndRaise(IntValueProperty, ref _intValue, value); + } + + public int IntValueOptimized + { + get => _intValue; + set => SetAndRaiseOptimized(IntValueProperty, ref _intValue, value); + } + + public int IntValueSimple + { + get => _intValue; + set + { + VerifyAccess(); + + if (_intValue == value) + { + return; + } + + var old = _intValue; + _intValue = value; + + RaisePropertyChanged(IntValueProperty, old, _intValue); + } + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Benchmarks/Base/Properties.cs b/tests/Avalonia.Benchmarks/Base/Properties.cs index 0a020961d5..45fc68ac96 100644 --- a/tests/Avalonia.Benchmarks/Base/Properties.cs +++ b/tests/Avalonia.Benchmarks/Base/Properties.cs @@ -1,5 +1,4 @@ -using System; -using System.Reactive.Subjects; +using System.Reactive.Subjects; using BenchmarkDotNet.Attributes; namespace Avalonia.Benchmarks.Base From 812c5f30c90d09473f40375ebe59e42641843608 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 21 Jul 2019 17:23:11 +0200 Subject: [PATCH 2/9] Remove SetAndRaise callback handling. Move event raising to setter. --- src/Avalonia.Base/AvaloniaObject.cs | 82 +++---------------- .../Utilities/AvaloniaPropertyCollection.cs | 4 +- .../Utilities/DeferredSetterOptimized.cs | 27 +++--- src/Avalonia.Base/ValueStore.cs | 14 ++-- .../Avalonia.Benchmarks.csproj | 2 +- .../Base/DirectPropertyBenchmark.cs | 2 +- 6 files changed, 36 insertions(+), 95 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 22aeccc3fb..99e27e7daf 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -466,7 +466,7 @@ namespace Avalonia /// The old property value. /// The new property value. /// The priority of the binding that produced the value. - protected void RaisePropertyChanged( + protected internal void RaisePropertyChanged( AvaloniaProperty property, object oldValue, object newValue, @@ -569,44 +569,6 @@ namespace Avalonia => SetAndRaiseCore(property, ref backing, val, notifyWrapper), value); } - - /// - /// Default setter handler that will set backing field and raise notification. - /// - private sealed class DefaultSetterHandler : DeferredSetterOptimized.ISetterHandler - { - public static readonly DefaultSetterHandler Instance = new DefaultSetterHandler(); - - public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) - { - var old = backing; - backing = value; - - source.RaisePropertyChanged(property, old, value); - - return true; - } - } - - /// - /// Setter handler that will run custom user callback. - /// - private sealed class CallbackSetterHandler : DeferredSetterOptimized.ISetterHandler - { - private readonly SetAndRaiseCallback _callback; - - public CallbackSetterHandler(SetAndRaiseCallback callback) - { - _callback = callback; - } - - public bool Update(AvaloniaObject source, AvaloniaProperty property, ref T backing, T value) - { - _callback(value, ref backing, notification => notification()); - - return true; - } - } protected bool SetAndRaiseOptimized(AvaloniaProperty property, ref T field, T value) { @@ -619,39 +581,21 @@ namespace Avalonia DeferredSetterOptimized setter = Values.GetDeferredSetter(property); - return setter.SetAndNotify(this, property, DefaultSetterHandler.Instance, ref field, value); - } - - protected bool SetAndRaiseOptimized( - AvaloniaProperty property, - ref T field, - SetAndRaiseCallback setterCallback, - T value) - { - VerifyAccess(); - - if (EqualityComparer.Default.Equals(field, value)) - { - return false; - } - - DeferredSetterOptimized setter = Values.GetDeferredSetter(property); - - return setter.SetAndNotify(this, property, new CallbackSetterHandler(setterCallback) , ref field, value); + return setter.SetAndNotify(this, property, ref field, value); } /// - /// Default assignment logic for SetAndRaise. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) + /// Default assignment logic for SetAndRaise. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// The value. + /// A wrapper for the property-changed notification. + /// + /// True if the value changed, otherwise false. + /// + private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) { var old = field; field = value; diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs index 5b1492a7a8..9b64342814 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs @@ -95,7 +95,7 @@ namespace Avalonia.Utilities return true; } - public void AddValueInternal(AvaloniaProperty property, TValue value) + public void AddValue(AvaloniaProperty property, TValue value) { Entry[] entries = new Entry[_entries.Length + 1]; @@ -117,7 +117,7 @@ namespace Avalonia.Utilities _entries = entries; } - public void SetValueInternal(AvaloniaProperty property, TValue value) + public void SetValue(AvaloniaProperty property, TValue value) { _entries[TryFindEntry(property.Id).Item1].Value = value; } diff --git a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs index e3bed90498..4858372828 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs @@ -17,20 +17,26 @@ namespace Avalonia.Utilities _pendingValues = new SingleOrQueue(); } + private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) + { + var old = backing; + + backing = value; + + source.RaisePropertyChanged(property, old, value); + } + public bool SetAndNotify( AvaloniaObject source, AvaloniaProperty property, - ISetterHandler handler, ref TSetRecord backing, TSetRecord value) { if (!_isNotifying) { - bool updated; - using (new NotifyDisposable(this)) { - updated = handler.Update(source, property, ref backing, value); + SetAndRaisePropertyChanged(source, property, ref backing, value); } if (!_pendingValues.Empty) @@ -39,12 +45,12 @@ namespace Avalonia.Utilities { while (!_pendingValues.Empty) { - updated = handler.Update(source, property, ref backing, _pendingValues.Dequeue()); + SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); } } } - return updated; + return true; } _pendingValues.Enqueue(value); @@ -71,14 +77,5 @@ namespace Avalonia.Utilities _setter._isNotifying = false; } } - - public interface ISetterHandler - { - bool Update( - AvaloniaObject source, - AvaloniaProperty property, - ref TSetRecord backing, - TSetRecord value); - } } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index b2c4c68f9c..b7f5c26801 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -33,13 +33,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValueInternal(property, priorityValue); + _propertyValues.SetValue(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - _propertyValues.AddValueInternal(property, priorityValue); + _propertyValues.AddValue(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -57,7 +57,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - _propertyValues.SetValueInternal(property, Validate(property, value)); + _propertyValues.SetValue(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -65,7 +65,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _propertyValues.SetValueInternal(property, priorityValue); + _propertyValues.SetValue(property, priorityValue); } } } @@ -78,14 +78,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - _propertyValues.AddValueInternal(property, Validate(property, value)); + _propertyValues.AddValue(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - _propertyValues.AddValueInternal(property, priorityValue); + _propertyValues.AddValue(property, priorityValue); } } @@ -187,7 +187,7 @@ namespace Avalonia var newDeferredSetter = new DeferredSetterOptimized(); - _deferredSetters.AddValueInternal(property, newDeferredSetter); + _deferredSetters.AddValue(property, newDeferredSetter); return newDeferredSetter; } diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 6550a23b7b..f503bf66a7 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -18,7 +18,7 @@ - + diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs index 4f36b54414..ecf8a1fb2b 100644 --- a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -79,4 +79,4 @@ namespace Avalonia.Benchmarks.Base } } } -} \ No newline at end of file +} From 48be9dc2600acf8a3269e6f93206b6b7d4f275cd Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sun, 21 Jul 2019 22:37:19 +0200 Subject: [PATCH 3/9] Remove old DeferredSetter implementation. Cleanup code, add license info. --- src/Avalonia.Base/AvaloniaObject.cs | 73 +------ ...ction.cs => AvaloniaPropertyValueStore.cs} | 13 +- src/Avalonia.Base/Utilities/DeferredSetter.cs | 180 +++++------------- .../Utilities/DeferredSetterOptimized.cs | 81 -------- src/Avalonia.Base/ValueStore.cs | 14 +- .../Base/DirectPropertyBenchmark.cs | 17 -- 6 files changed, 66 insertions(+), 312 deletions(-) rename src/Avalonia.Base/Utilities/{AvaloniaPropertyCollection.cs => AvaloniaPropertyValueStore.cs} (91%) delete mode 100644 src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 99e27e7daf..85dccd53cb 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -508,45 +508,6 @@ namespace Avalonia } } - /// - /// A callback type for encapsulating complex logic for setting direct properties. - /// - /// The type of the property. - /// The value to which to set the property. - /// The backing field for the property. - /// A wrapper for the property-changed notification. - protected delegate void SetAndRaiseCallback(T value, ref T field, Action notifyWrapper); - - /// - /// Sets the backing field for a direct avalonia property, raising the - /// event if the value has changed. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// A callback called to actually set the value to the backing field. - /// The value. - /// - /// True if the value changed, otherwise false. - /// - protected bool SetAndRaise( - AvaloniaProperty property, - ref T field, - SetAndRaiseCallback setterCallback, - T value) - { - Contract.Requires(setterCallback != null); - return Values.Setter.SetAndNotify( - property, - ref field, - (object update, ref T backing, Action notify) => - { - setterCallback((T)update, ref backing, notify); - return true; - }, - value); - } - /// /// Sets the backing field for a direct avalonia property, raising the /// event if the value has changed. @@ -562,48 +523,16 @@ namespace Avalonia { VerifyAccess(); - return SetAndRaise( - property, - ref field, - (T val, ref T backing, Action notifyWrapper) - => SetAndRaiseCore(property, ref backing, val, notifyWrapper), - value); - } - - protected bool SetAndRaiseOptimized(AvaloniaProperty property, ref T field, T value) - { - VerifyAccess(); - if (EqualityComparer.Default.Equals(field, value)) { return false; } - DeferredSetterOptimized setter = Values.GetDeferredSetter(property); + DeferredSetter setter = Values.GetDeferredSetter(property); return setter.SetAndNotify(this, property, ref field, value); } - /// - /// Default assignment logic for SetAndRaise. - /// - /// The type of the property. - /// The property. - /// The backing field. - /// The value. - /// A wrapper for the property-changed notification. - /// - /// True if the value changed, otherwise false. - /// - private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) - { - var old = field; - field = value; - - notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue)); - return true; - } - /// /// Tries to cast a value to a type, taking into account that the value may be a /// . diff --git a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs similarity index 91% rename from src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs rename to src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs index 9b64342814..ac128d83de 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaPropertyCollection.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaPropertyValueStore.cs @@ -1,13 +1,20 @@ -using System; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; using System.Collections.Generic; namespace Avalonia.Utilities { - internal sealed class AvaloniaPropertyCollection + /// + /// Stores values with as key. + /// + /// Stored value type. + internal sealed class AvaloniaPropertyValueStore { private Entry[] _entries; - public AvaloniaPropertyCollection() + public AvaloniaPropertyValueStore() { // The last item in the list is always int.MaxValue _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = default } }; diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index 1b1324b1c5..54458d6e6a 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -1,5 +1,7 @@ -using System; -using System.Collections.Generic; +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; namespace Avalonia.Utilities { @@ -8,161 +10,75 @@ namespace Avalonia.Utilities /// Used to fix #855. /// /// The type of value with which to track the delayed assignment. - class DeferredSetter + internal sealed class DeferredSetter { - private struct NotifyDisposable : IDisposable + private readonly SingleOrQueue _pendingValues; + private bool _isNotifying; + + public DeferredSetter() { - private readonly SettingStatus status; + _pendingValues = new SingleOrQueue(); + } - internal NotifyDisposable(SettingStatus status) - { - this.status = status; - status.Notifying = true; - } + private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) + { + var old = backing; - public void Dispose() - { - status.Notifying = false; - } + backing = value; + + source.RaisePropertyChanged(property, old, value); } - /// - /// Information on current setting/notification status of a property. - /// - private class SettingStatus + public bool SetAndNotify( + AvaloniaObject source, + AvaloniaProperty property, + ref TSetRecord backing, + TSetRecord value) { - public bool Notifying { get; set; } - - private SingleOrQueue pendingValues; - - public SingleOrQueue PendingValues + if (!_isNotifying) { - get + using (new NotifyDisposable(this)) { - return pendingValues ?? (pendingValues = new SingleOrQueue()); + SetAndRaisePropertyChanged(source, property, ref backing, value); } - } - } - private Dictionary _setRecords; - private Dictionary SetRecords - => _setRecords ?? (_setRecords = new Dictionary()); + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); + } + } + } - private SettingStatus GetOrCreateStatus(AvaloniaProperty property) - { - if (!SetRecords.TryGetValue(property, out var status)) - { - status = new SettingStatus(); - SetRecords.Add(property, status); + return true; } - return status; - } - - /// - /// Mark the property as currently notifying. - /// - /// The property to mark as notifying. - /// Returns a disposable that when disposed, marks the property as done notifying. - private NotifyDisposable MarkNotifying(AvaloniaProperty property) - { - Contract.Requires(!IsNotifying(property)); - - SettingStatus status = GetOrCreateStatus(property); - - return new NotifyDisposable(status); - } - - /// - /// Check if the property is currently notifying listeners. - /// - /// The property. - /// If the property is currently notifying listeners. - private bool IsNotifying(AvaloniaProperty property) - => SetRecords.TryGetValue(property, out var value) && value.Notifying; - - /// - /// Add a pending assignment for the property. - /// - /// The property. - /// The value to assign. - private void AddPendingSet(AvaloniaProperty property, TSetRecord value) - { - Contract.Requires(IsNotifying(property)); - - GetOrCreateStatus(property).PendingValues.Enqueue(value); - } + _pendingValues.Enqueue(value); - /// - /// Checks if there are any pending assignments for the property. - /// - /// The property to check. - /// If the property has any pending assignments. - private bool HasPendingSet(AvaloniaProperty property) - { - return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty; + return false; } /// - /// Gets the first pending assignment for the property. + /// Disposable that marks the property as currently notifying. + /// When disposed, marks the property as done notifying. /// - /// The property to check. - /// The first pending assignment for the property. - private TSetRecord GetFirstPendingSet(AvaloniaProperty property) + private readonly struct NotifyDisposable : IDisposable { - return GetOrCreateStatus(property).PendingValues.Dequeue(); - } - - public delegate bool SetterDelegate(TSetRecord record, ref TValue backing, Action notifyCallback); + private readonly DeferredSetter _setter; - /// - /// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824 - /// - /// The property to set. - /// The backing field for the property - /// - /// A callback that actually sets the property. - /// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification. - /// - /// The value to try to set. - public bool SetAndNotify( - AvaloniaProperty property, - ref TValue backing, - SetterDelegate setterCallback, - TSetRecord value) - { - Contract.Requires(setterCallback != null); - if (!IsNotifying(property)) + internal NotifyDisposable(DeferredSetter setter) { - bool updated = false; - if (!object.Equals(value, backing)) - { - updated = setterCallback(value, ref backing, notification => - { - using (MarkNotifying(property)) - { - notification(); - } - }); - } - while (HasPendingSet(property)) - { - updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification => - { - using (MarkNotifying(property)) - { - notification(); - } - }); - } - - return updated; + _setter = setter; + _setter._isNotifying = true; } - else if(!object.Equals(value, backing)) + + public void Dispose() { - AddPendingSet(property, value); + _setter._isNotifying = false; } - return false; } } } diff --git a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs b/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs deleted file mode 100644 index 4858372828..0000000000 --- a/src/Avalonia.Base/Utilities/DeferredSetterOptimized.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; - -namespace Avalonia.Utilities -{ - /// - /// A utility class to enable deferring assignment until after property-changed notifications are sent. - /// Used to fix #855. - /// - /// The type of value with which to track the delayed assignment. - internal sealed class DeferredSetterOptimized - { - private bool _isNotifying; - private readonly SingleOrQueue _pendingValues; - - public DeferredSetterOptimized() - { - _pendingValues = new SingleOrQueue(); - } - - private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty property, ref TSetRecord backing, TSetRecord value) - { - var old = backing; - - backing = value; - - source.RaisePropertyChanged(property, old, value); - } - - public bool SetAndNotify( - AvaloniaObject source, - AvaloniaProperty property, - ref TSetRecord backing, - TSetRecord value) - { - if (!_isNotifying) - { - using (new NotifyDisposable(this)) - { - SetAndRaisePropertyChanged(source, property, ref backing, value); - } - - if (!_pendingValues.Empty) - { - using (new NotifyDisposable(this)) - { - while (!_pendingValues.Empty) - { - SetAndRaisePropertyChanged(source, property, ref backing, _pendingValues.Dequeue()); - } - } - } - - return true; - } - - _pendingValues.Enqueue(value); - - return false; - } - - /// - /// Disposable that marks the property as currently notifying. - /// When disposed, marks the property as done notifying. - /// - private readonly struct NotifyDisposable : IDisposable - { - private readonly DeferredSetterOptimized _setter; - - internal NotifyDisposable(DeferredSetterOptimized setter) - { - _setter = setter; - _setter._isNotifying = true; - } - - public void Dispose() - { - _setter._isNotifying = false; - } - } - } -} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index b7f5c26801..8dfabc71a2 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,15 +7,15 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - private readonly AvaloniaPropertyCollection _propertyValues; - private readonly AvaloniaPropertyCollection _deferredSetters; + private readonly AvaloniaPropertyValueStore _propertyValues; + private readonly AvaloniaPropertyValueStore _deferredSetters; private readonly AvaloniaObject _owner; public ValueStore(AvaloniaObject owner) { _owner = owner; - _propertyValues = new AvaloniaPropertyCollection(); - _deferredSetters = new AvaloniaPropertyCollection(); + _propertyValues = new AvaloniaPropertyValueStore(); + _deferredSetters = new AvaloniaPropertyValueStore(); } public IDisposable AddBinding( @@ -178,14 +178,14 @@ namespace Avalonia return value; } - public DeferredSetterOptimized GetDeferredSetter(AvaloniaProperty property) + public DeferredSetter GetDeferredSetter(AvaloniaProperty property) { if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { - return (DeferredSetterOptimized)deferredSetter; + return (DeferredSetter)deferredSetter; } - var newDeferredSetter = new DeferredSetterOptimized(); + var newDeferredSetter = new DeferredSetter(); _deferredSetters.AddValue(property, newDeferredSetter); diff --git a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs index ecf8a1fb2b..02f9397e2c 100644 --- a/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs +++ b/tests/Avalonia.Benchmarks/Base/DirectPropertyBenchmark.cs @@ -16,17 +16,6 @@ namespace Avalonia.Benchmarks.Base } } - [Benchmark] - public void SetAndRaiseOptimized() - { - var obj = new DirectClass(); - - for (var i = 0; i < 100; ++i) - { - obj.IntValueOptimized += 1; - } - } - [Benchmark] public void SetAndRaiseSimple() { @@ -53,12 +42,6 @@ namespace Avalonia.Benchmarks.Base set => SetAndRaise(IntValueProperty, ref _intValue, value); } - public int IntValueOptimized - { - get => _intValue; - set => SetAndRaiseOptimized(IntValueProperty, ref _intValue, value); - } - public int IntValueSimple { get => _intValue; From ad994a685cd6a8778bfebc6bcc77fce2fbd9a704 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Mon, 22 Jul 2019 00:19:45 +0200 Subject: [PATCH 4/9] Fix PriorityValue usage of DeferredSetter. Implement slower path in DeferredSetter that deals with callbacks. --- src/Avalonia.Base/AvaloniaObject.cs | 2 +- src/Avalonia.Base/IPriorityValueOwner.cs | 9 ++++- src/Avalonia.Base/PriorityValue.cs | 34 +++++++++-------- src/Avalonia.Base/Utilities/DeferredSetter.cs | 38 +++++++++++++++++++ src/Avalonia.Base/ValueStore.cs | 15 ++++---- .../PriorityValueTests.cs | 2 +- 6 files changed, 72 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 85dccd53cb..a3d5803ab0 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -528,7 +528,7 @@ namespace Avalonia return false; } - DeferredSetter setter = Values.GetDeferredSetter(property); + DeferredSetter setter = Values.GetDirectDeferredSetter(property); return setter.SetAndNotify(this, property, ref field, value); } diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 540b1bf19b..1d6e5e59ad 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/src/Avalonia.Base/IPriorityValueOwner.cs @@ -29,6 +29,13 @@ namespace Avalonia /// The notification. void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification); + /// + /// Returns deferred setter for given non-direct property. + /// + /// Property. + /// Deferred setter for given property. + DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property); + /// /// Logs a binding error. /// @@ -40,7 +47,5 @@ namespace Avalonia /// Ensures that the current thread is the UI thread. /// void VerifyAccess(); - - DeferredSetter Setter { get; } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 89a893577f..4996420fe7 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -30,7 +30,9 @@ namespace Avalonia private readonly SingleOrDictionary _levels = new SingleOrDictionary(); private readonly Func _validate; + private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback; private (object value, int priority) _value; + private DeferredSetter _setter; /// /// Initializes a new instance of the class. @@ -50,6 +52,7 @@ namespace Avalonia _valueType = valueType; _value = (AvaloniaProperty.UnsetValue, int.MaxValue); _validate = validate; + _setAndNotifyCallback = SetAndNotify; } /// @@ -242,22 +245,22 @@ namespace Avalonia /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - Owner.Setter.SetAndNotify(Property, - ref _value, - UpdateCore, - (value, priority)); - } + var newValue = (value, priority); + + if (newValue == _value) + { + return; + } - private bool UpdateCore( - object update, - ref (object value, int priority) backing, - Action notify) - => UpdateCore(((object, int))update, ref backing, notify); + if (_setter == null) + { + _setter = Owner.GetNonDirectDeferredSetter(Property); + } + + _setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue); + } - private bool UpdateCore( - (object value, int priority) update, - ref (object value, int priority) backing, - Action notify) + private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update) { var val = update.value; var notification = val as BindingNotification; @@ -286,7 +289,7 @@ namespace Avalonia if (notification == null || notification.HasValue) { - notify(() => Owner?.Changed(Property, ValuePriority, old, Value)); + Owner?.Changed(Property, ValuePriority, old, Value); } if (notification != null) @@ -305,7 +308,6 @@ namespace Avalonia val, val?.GetType()); } - return true; } } } diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index 54458d6e6a..fd7a66fb52 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -5,6 +5,15 @@ using System; namespace Avalonia.Utilities { + /// + /// Callback invoked when deferred setter wants to set a value. + /// + /// Value type. + /// Property being set. + /// Backing field reference. + /// New value. + internal delegate void SetAndNotifyCallback(AvaloniaProperty property, ref TValue backing, TValue value); + /// /// A utility class to enable deferring assignment until after property-changed notifications are sent. /// Used to fix #855. @@ -61,6 +70,35 @@ namespace Avalonia.Utilities return false; } + public bool SetAndNotifyCallback(AvaloniaProperty property, SetAndNotifyCallback setAndNotifyCallback, ref TValue backing, TValue value) + where TValue : TSetRecord + { + if (!_isNotifying) + { + using (new NotifyDisposable(this)) + { + setAndNotifyCallback(property, ref backing, value); + } + + if (!_pendingValues.Empty) + { + using (new NotifyDisposable(this)) + { + while (!_pendingValues.Empty) + { + setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); + } + } + } + + return true; + } + + _pendingValues.Enqueue(value); + + return false; + } + /// /// Disposable that marks the property as currently notifying. /// When disposed, marks the property as done notifying. diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 8dfabc71a2..1bdbd4ca7c 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -178,7 +178,7 @@ namespace Avalonia return value; } - public DeferredSetter GetDeferredSetter(AvaloniaProperty property) + private DeferredSetter GetDeferredSetter(AvaloniaProperty property) { if (_deferredSetters.TryGetValue(property, out var deferredSetter)) { @@ -192,15 +192,14 @@ namespace Avalonia return newDeferredSetter; } - private DeferredSetter _deferredSetter; + public DeferredSetter GetNonDirectDeferredSetter(AvaloniaProperty property) + { + return GetDeferredSetter(property); + } - public DeferredSetter Setter + public DeferredSetter GetDirectDeferredSetter(AvaloniaProperty property) { - get - { - return _deferredSetter ?? - (_deferredSetter = new DeferredSetter()); - } + return GetDeferredSetter(property); } } } diff --git a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs index 2f1b7862a7..63e1790cce 100644 --- a/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Avalonia.Base.UnitTests/PriorityValueTests.cs @@ -307,7 +307,7 @@ namespace Avalonia.Base.UnitTests private static Mock GetMockOwner() { var owner = new Mock(); - owner.SetupGet(o => o.Setter).Returns(new DeferredSetter()); + owner.Setup(o => o.GetNonDirectDeferredSetter(It.IsAny())).Returns(new DeferredSetter()); return owner; } } From a48bc262af183e58a82cad1116a101a0b4e4e183 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 23 Jul 2019 00:01:02 +0300 Subject: [PATCH 5/9] Support touch input for fbdev via libinput --- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 3 +- src/Avalonia.Input/TouchDevice.cs | 8 ++ .../Avalonia.LinuxFramebuffer/EvDevDevice.cs | 88 ------------ .../FramebufferToplevelImpl.cs | 14 +- .../Input/IInputBackend.cs | 12 ++ .../Input/IScreenInfoProvider.cs | 7 + .../Input/LibInput/LibInputBackend.cs | 125 ++++++++++++++++++ .../LibInput/LibInputNativeUnsafeMethods.cs | 122 +++++++++++++++++ .../LinuxFramebufferPlatform.cs | 3 +- src/Linux/Avalonia.LinuxFramebuffer/Mice.cs | 117 ---------------- .../NativeUnsafeMethods.cs | 2 +- 11 files changed, 288 insertions(+), 213 deletions(-) delete mode 100644 src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs delete mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Mice.cs diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index b728844e97..6fac90f255 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -19,7 +19,8 @@ namespace Avalonia.Input.Raw NonClientLeftButtonDown, TouchBegin, TouchUpdate, - TouchEnd + TouchEnd, + TouchCancel } /// diff --git a/src/Avalonia.Input/TouchDevice.cs b/src/Avalonia.Input/TouchDevice.cs index 7f473bb320..c85f98b04a 100644 --- a/src/Avalonia.Input/TouchDevice.cs +++ b/src/Avalonia.Input/TouchDevice.cs @@ -61,6 +61,12 @@ namespace Avalonia.Input pointer.IsPrimary ? MouseButton.Left : MouseButton.None)); } } + if (args.Type == RawPointerEventType.TouchCancel) + { + _pointers.Remove(args.TouchPointId); + using (pointer) + pointer.Capture(null); + } if (args.Type == RawPointerEventType.TouchUpdate) { @@ -68,6 +74,8 @@ namespace Avalonia.Input target.RaiseEvent(new PointerEventArgs(InputElement.PointerMovedEvent, target, pointer, args.Root, args.Position, ev.Timestamp, new PointerPointProperties(modifiers), modifiers)); } + + } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs b/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs deleted file mode 100644 index f28dca81b8..0000000000 --- a/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs +++ /dev/null @@ -1,88 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; - -namespace Avalonia.LinuxFramebuffer -{ - unsafe class EvDevDevice - { - private static readonly Lazy> AllMouseDevices = new Lazy>(() - => OpenMouseDevices()); - - private static List OpenMouseDevices() - { - var rv = new List(); - foreach (var dev in Directory.GetFiles("/dev/input", "event*").Select(Open)) - { - if (!dev.IsMouse) - NativeUnsafeMethods.close(dev.Fd); - else - rv.Add(dev); - } - return rv; - } - - public static IReadOnlyList MouseDevices => AllMouseDevices.Value; - - - public int Fd { get; } - private IntPtr _dev; - public string Name { get; } - public List EventTypes { get; private set; } = new List(); - public input_absinfo? AbsX { get; } - public input_absinfo? AbsY { get; } - - public EvDevDevice(int fd, IntPtr dev) - { - Fd = fd; - _dev = dev; - Name = Marshal.PtrToStringAnsi(NativeUnsafeMethods.libevdev_get_name(_dev)); - foreach (EvType type in Enum.GetValues(typeof(EvType))) - { - if (NativeUnsafeMethods.libevdev_has_event_type(dev, type) != 0) - EventTypes.Add(type); - } - var ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int) AbsAxis.ABS_X); - if (ptr != null) - AbsX = *ptr; - ptr = NativeUnsafeMethods.libevdev_get_abs_info(dev, (int)AbsAxis.ABS_Y); - if (ptr != null) - AbsY = *ptr; - } - - public input_event? NextEvent() - { - input_event ev; - if (NativeUnsafeMethods.libevdev_next_event(_dev, 2, out ev) == 0) - return ev; - return null; - } - - public bool IsMouse => EventTypes.Contains(EvType.EV_REL); - - public static EvDevDevice Open(string device) - { - var fd = NativeUnsafeMethods.open(device, 2048, 0); - if (fd <= 0) - throw new Exception($"Unable to open {device} code {Marshal.GetLastWin32Error()}"); - IntPtr dev; - var rc = NativeUnsafeMethods.libevdev_new_from_fd(fd, out dev); - if (rc < 0) - { - NativeUnsafeMethods.close(fd); - throw new Exception($"Unable to initialize evdev for {device} code {Marshal.GetLastWin32Error()}"); - } - return new EvDevDevice(fd, dev); - } - - - } - - public class EvDevAxisInfo - { - public int Minimum { get; set; } - public int Maximum { get; set; } - } -} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 78369a3648..9b9bd6c7b8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -2,25 +2,26 @@ using System.Collections.Generic; using Avalonia.Input; using Avalonia.Input.Raw; +using Avalonia.LinuxFramebuffer.Input; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; namespace Avalonia.LinuxFramebuffer { - class FramebufferToplevelImpl : IEmbeddableWindowImpl + class FramebufferToplevelImpl : IEmbeddableWindowImpl, IScreenInfoProvider { private readonly LinuxFramebuffer _fb; + private readonly IInputBackend _inputBackend; private bool _renderQueued; public IInputRoot InputRoot { get; private set; } - public FramebufferToplevelImpl(LinuxFramebuffer fb) + public FramebufferToplevelImpl(LinuxFramebuffer fb, IInputBackend inputBackend) { _fb = fb; + _inputBackend = inputBackend; Invalidate(default(Rect)); - var mice = new Mice(this, ClientSize.Width, ClientSize.Height); - mice.Start(); - mice.Event += e => Input?.Invoke(e); + _inputBackend.Initialize(this, e => Input?.Invoke(e)); } public IRenderer CreateRenderer(IRenderRoot root) @@ -49,6 +50,7 @@ namespace Avalonia.LinuxFramebuffer public void SetInputRoot(IInputRoot inputRoot) { InputRoot = inputRoot; + _inputBackend.SetInputRoot(inputRoot); } public Point PointToClient(PixelPoint p) => p.ToPoint(1); @@ -73,5 +75,7 @@ namespace Avalonia.LinuxFramebuffer add {} remove {} } + + public Size ScaledSize => _fb.PixelSize / Scaling; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs new file mode 100644 index 0000000000..84a903bb9d --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IInputBackend.cs @@ -0,0 +1,12 @@ +using System; +using Avalonia.Input; +using Avalonia.Input.Raw; + +namespace Avalonia.LinuxFramebuffer.Input +{ + public interface IInputBackend + { + void Initialize(IScreenInfoProvider info, Action onInput); + void SetInputRoot(IInputRoot root); + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs new file mode 100644 index 0000000000..cb0e51862a --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/IScreenInfoProvider.cs @@ -0,0 +1,7 @@ +namespace Avalonia.LinuxFramebuffer.Input +{ + public interface IScreenInfoProvider + { + Size ScaledSize { get; } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs new file mode 100644 index 0000000000..ca31db6100 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Threading; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Threading; +using static Avalonia.LinuxFramebuffer.Input.LibInput.LibInputNativeUnsafeMethods; +namespace Avalonia.LinuxFramebuffer.Input.LibInput +{ + public class LibInputBackend : IInputBackend + { + private IScreenInfoProvider _screen; + private IInputRoot _inputRoot; + private readonly Queue _inputThreadActions = new Queue(); + private TouchDevice _touch = new TouchDevice(); + + private readonly Queue _inputQueue = new Queue(); + private Action _onInput; + private Dictionary _pointers = new Dictionary(); + + public LibInputBackend() + { + var ctx = libinput_path_create_context(); + + new Thread(()=>InputThread(ctx)).Start(); + } + + + + private unsafe void InputThread(IntPtr ctx) + { + var fd = libinput_get_fd(ctx); + + var timeval = stackalloc IntPtr[2]; + + + foreach (var f in Directory.GetFiles("/dev/input", "event*")) + libinput_path_add_device(ctx, f); + while (true) + { + IntPtr ev; + while ((ev = libinput_get_event(ctx)) != IntPtr.Zero) + { + + var type = libinput_event_get_type(ev); + if (type >= LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN && + type <= LibInputEventType.LIBINPUT_EVENT_TOUCH_CANCEL) + HandleTouch(ev, type); + + libinput_event_destroy(ev); + } + libinput_dispatch(ctx); + } + } + + private void ScheduleInput(RawInputEventArgs ev) + { + _inputQueue.Enqueue(ev); + if (_inputQueue.Count == 1) + { + Dispatcher.UIThread.Post(() => + { + while (_inputQueue.Count > 0) + { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); + var dequeuedEvent = _inputQueue.Dequeue(); + _onInput?.Invoke(dequeuedEvent); + } + }, DispatcherPriority.Input); + } + } + + private void HandleTouch(IntPtr ev, LibInputEventType type) + { + var tev = libinput_event_get_touch_event(ev); + if(tev == IntPtr.Zero) + return; + if (type < LibInputEventType.LIBINPUT_EVENT_TOUCH_FRAME) + { + var info = _screen.ScaledSize; + var slot = libinput_event_touch_get_slot(tev); + Point pt; + + if (type == LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN + || type == LibInputEventType.LIBINPUT_EVENT_TOUCH_MOTION) + { + var x = libinput_event_touch_get_x_transformed(tev, (int)info.Width); + var y = libinput_event_touch_get_y_transformed(tev, (int)info.Height); + pt = new Point(x, y); + _pointers[slot] = pt; + } + else + { + _pointers.TryGetValue(slot, out pt); + _pointers.Remove(slot); + } + + var ts = libinput_event_touch_get_time_usec(tev) / 1000; + if (_inputRoot == null) + return; + ScheduleInput(new RawTouchEventArgs(_touch, ts, + _inputRoot, + type == LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN ? RawPointerEventType.TouchBegin + : type == LibInputEventType.LIBINPUT_EVENT_TOUCH_UP ? RawPointerEventType.TouchEnd + : type == LibInputEventType.LIBINPUT_EVENT_TOUCH_MOTION ? RawPointerEventType.TouchUpdate + : RawPointerEventType.TouchCancel, + pt, InputModifiers.None, slot)); + } + } + + + public void Initialize(IScreenInfoProvider screen, Action onInput) + { + _screen = screen; + _onInput = onInput; + } + + public void SetInputRoot(IInputRoot root) + { + _inputRoot = root; + } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs new file mode 100644 index 0000000000..6c82f53071 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs @@ -0,0 +1,122 @@ +using System; +using System.Runtime.InteropServices; + +namespace Avalonia.LinuxFramebuffer.Input.LibInput +{ + unsafe class LibInputNativeUnsafeMethods + { + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + delegate int OpenRestrictedCallbackDelegate(IntPtr path, int flags, IntPtr userData); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + delegate void CloseRestrictedCallbackDelegate(int fd, IntPtr userData); + + static int OpenRestricted(IntPtr path, int flags, IntPtr userData) + { + var fd = NativeUnsafeMethods.open(Marshal.PtrToStringAnsi(path), flags, 0); + if (fd == -1) + return -Marshal.GetLastWin32Error(); + + return fd; + } + + static void CloseRestricted(int fd, IntPtr userData) + { + NativeUnsafeMethods.close(fd); + } + + private static readonly IntPtr* s_Interface; + + static LibInputNativeUnsafeMethods() + { + s_Interface = (IntPtr*)Marshal.AllocHGlobal(IntPtr.Size * 2); + + IntPtr Convert(TDelegate del) + { + GCHandle.Alloc(del); + return Marshal.GetFunctionPointerForDelegate(del); + } + + s_Interface[0] = Convert(OpenRestricted); + s_Interface[1] = Convert(CloseRestricted); + } + + private const string LibInput = "libinput.so.10"; + + [DllImport(LibInput)] + public extern static IntPtr libinput_path_create_context(IntPtr* iface, IntPtr userData); + + public static IntPtr libinput_path_create_context() => + libinput_path_create_context(s_Interface, IntPtr.Zero); + + [DllImport(LibInput)] + public extern static IntPtr libinput_path_add_device(IntPtr ctx, [MarshalAs(UnmanagedType.LPStr)] string path); + + [DllImport(LibInput)] + public extern static IntPtr libinput_path_remove_device(IntPtr device); + + [DllImport(LibInput)] + public extern static int libinput_get_fd(IntPtr ctx); + + [DllImport(LibInput)] + public extern static void libinput_dispatch(IntPtr ctx); + + [DllImport(LibInput)] + public extern static IntPtr libinput_get_event(IntPtr ctx); + + [DllImport(LibInput)] + public extern static LibInputEventType libinput_event_get_type(IntPtr ev); + + public enum LibInputEventType + { + LIBINPUT_EVENT_NONE = 0, + LIBINPUT_EVENT_DEVICE_ADDED, + LIBINPUT_EVENT_DEVICE_REMOVED, + LIBINPUT_EVENT_KEYBOARD_KEY = 300, + LIBINPUT_EVENT_POINTER_MOTION = 400, + LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE, + LIBINPUT_EVENT_POINTER_BUTTON, + LIBINPUT_EVENT_POINTER_AXIS, + LIBINPUT_EVENT_TOUCH_DOWN = 500, + LIBINPUT_EVENT_TOUCH_UP, + LIBINPUT_EVENT_TOUCH_MOTION, + LIBINPUT_EVENT_TOUCH_CANCEL, + LIBINPUT_EVENT_TOUCH_FRAME, + LIBINPUT_EVENT_TABLET_TOOL_AXIS = 600, + LIBINPUT_EVENT_TABLET_TOOL_PROXIMITY, + LIBINPUT_EVENT_TABLET_TOOL_TIP, + LIBINPUT_EVENT_TABLET_TOOL_BUTTON, + LIBINPUT_EVENT_TABLET_PAD_BUTTON = 700, + LIBINPUT_EVENT_TABLET_PAD_RING, + LIBINPUT_EVENT_TABLET_PAD_STRIP, + LIBINPUT_EVENT_GESTURE_SWIPE_BEGIN = 800, + LIBINPUT_EVENT_GESTURE_SWIPE_UPDATE, + LIBINPUT_EVENT_GESTURE_SWIPE_END, + LIBINPUT_EVENT_GESTURE_PINCH_BEGIN, + LIBINPUT_EVENT_GESTURE_PINCH_UPDATE, + LIBINPUT_EVENT_GESTURE_PINCH_END, + LIBINPUT_EVENT_SWITCH_TOGGLE = 900, + } + + + [DllImport(LibInput)] + public extern static void libinput_event_destroy(IntPtr ev); + + [DllImport(LibInput)] + public extern static IntPtr libinput_event_get_touch_event(IntPtr ev); + + [DllImport(LibInput)] + public extern static int libinput_event_touch_get_slot(IntPtr ev); + + [DllImport(LibInput)] + public extern static ulong libinput_event_touch_get_time_usec(IntPtr ev); + + [DllImport(LibInput)] + public extern static double libinput_event_touch_get_x_transformed(IntPtr ev, int width); + + [DllImport(LibInput)] + public extern static double libinput_event_touch_get_y_transformed(IntPtr ev, int height); + + + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 396942c8dd..1eea1c07f7 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.LinuxFramebuffer; +using Avalonia.LinuxFramebuffer.Input.LibInput; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -69,7 +70,7 @@ namespace Avalonia.LinuxFramebuffer if (_topLevel == null) { - var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb)); + var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, new LibInputBackend())); tl.Prepare(); _topLevel = tl; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs deleted file mode 100644 index 2b82b4f4aa..0000000000 --- a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs +++ /dev/null @@ -1,117 +0,0 @@ -using System; -using System.Linq; -using System.Threading; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Platform; - -namespace Avalonia.LinuxFramebuffer -{ - unsafe class Mice - { - private readonly FramebufferToplevelImpl _topLevel; - private readonly double _width; - private readonly double _height; - private double _x; - private double _y; - - public event Action Event; - - public Mice(FramebufferToplevelImpl topLevel, double width, double height) - { - _topLevel = topLevel; - _width = width; - _height = height; - } - - public void Start() => ThreadPool.UnsafeQueueUserWorkItem(_ => Worker(), null); - - private void Worker() - { - - var mouseDevices = EvDevDevice.MouseDevices.Where(d => d.IsMouse).ToList(); - if (mouseDevices.Count == 0) - return; - var are = new AutoResetEvent(false); - while (true) - { - try - { - var rfds = new fd_set {count = mouseDevices.Count}; - for (int c = 0; c < mouseDevices.Count; c++) - rfds.fds[c] = mouseDevices[c].Fd; - IntPtr* timeval = stackalloc IntPtr[2]; - timeval[0] = new IntPtr(0); - timeval[1] = new IntPtr(100); - are.WaitOne(30); - foreach (var dev in mouseDevices) - { - while(true) - { - var ev = dev.NextEvent(); - if (!ev.HasValue) - break; - - LinuxFramebufferPlatform.Threading.Send(() => ProcessEvent(dev, ev.Value)); - } - } - } - catch (Exception e) - { - Console.Error.WriteLine(e.ToString()); - } - } - } - - static double TranslateAxis(input_absinfo axis, int value, double max) - { - return (value - axis.minimum) / (double) (axis.maximum - axis.minimum) * max; - } - - private void ProcessEvent(EvDevDevice device, input_event ev) - { - if (ev.type == (short)EvType.EV_REL) - { - if (ev.code == (short) AxisEventCode.REL_X) - _x = Math.Min(_width, Math.Max(0, _x + ev.value)); - else if (ev.code == (short) AxisEventCode.REL_Y) - _y = Math.Min(_height, Math.Max(0, _y + ev.value)); - else - return; - Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice, - LinuxFramebufferPlatform.Timestamp, - _topLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y), - InputModifiers.None)); - } - if (ev.type ==(int) EvType.EV_ABS) - { - if (ev.code == (short) AbsAxis.ABS_X && device.AbsX.HasValue) - _x = TranslateAxis(device.AbsX.Value, ev.value, _width); - else if (ev.code == (short) AbsAxis.ABS_Y && device.AbsY.HasValue) - _y = TranslateAxis(device.AbsY.Value, ev.value, _height); - else - return; - Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice, - LinuxFramebufferPlatform.Timestamp, - _topLevel.InputRoot, RawPointerEventType.Move, new Point(_x, _y), - InputModifiers.None)); - } - if (ev.type == (short) EvType.EV_KEY) - { - RawPointerEventType? type = null; - if (ev.code == (ushort) EvKey.BTN_LEFT) - type = ev.value == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp; - if (ev.code == (ushort)EvKey.BTN_RIGHT) - type = ev.value == 1 ? RawPointerEventType.RightButtonDown : RawPointerEventType.RightButtonUp; - if (ev.code == (ushort) EvKey.BTN_MIDDLE) - type = ev.value == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp; - if (!type.HasValue) - return; - - Event?.Invoke(new RawPointerEventArgs(LinuxFramebufferPlatform.MouseDevice, - LinuxFramebufferPlatform.Timestamp, - _topLevel.InputRoot, type.Value, new Point(_x, _y), default(InputModifiers))); - } - } - } -} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs index 5427af7d44..8fbd78588f 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs @@ -188,7 +188,7 @@ namespace Avalonia.LinuxFramebuffer unsafe struct fd_set { public int count; - public fixed int fds [256]; + public fixed byte fds [256]; } enum AxisEventCode From e9baedcbfc6443ef5150a657af6b8de2c89ee573 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 23 Jul 2019 00:55:29 +0300 Subject: [PATCH 6/9] Fixed platform threading for fbdev --- .../InternalPlatformThreadingInterface.cs | 85 +++++++------------ .../Remote/PreviewerWindowingPlatform.cs | 2 +- .../FramebufferToplevelImpl.cs | 12 +-- .../Input/LibInput/LibInputBackend.cs | 33 ++++--- .../LinuxFramebufferPlatform.cs | 14 +-- .../NativeUnsafeMethods.cs | 11 +++ 6 files changed, 75 insertions(+), 82 deletions(-) diff --git a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs index bb357453ff..cb1291410a 100644 --- a/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs +++ b/src/Avalonia.Controls/Platform/InternalPlatformThreadingInterface.cs @@ -9,94 +9,69 @@ using Avalonia.Threading; namespace Avalonia.Controls.Platform { - public class InternalPlatformThreadingInterface : IPlatformThreadingInterface, IRenderTimer + public class InternalPlatformThreadingInterface : IPlatformThreadingInterface { public InternalPlatformThreadingInterface() { TlsCurrentThreadIsLoopThread = true; - StartTimer( - DispatcherPriority.Render, - new TimeSpan(0, 0, 0, 0, 66), - () => Tick?.Invoke(TimeSpan.FromMilliseconds(Environment.TickCount))); } private readonly AutoResetEvent _signaled = new AutoResetEvent(false); - private readonly AutoResetEvent _queued = new AutoResetEvent(false); - private readonly Queue _actions = new Queue(); public void RunLoop(CancellationToken cancellationToken) { - var handles = new[] {_signaled, _queued}; while (true) { - if (0 == WaitHandle.WaitAny(handles)) - Signaled?.Invoke(null); - else - { - while (true) - { - Action item; - lock (_actions) - if (_actions.Count == 0) - break; - else - item = _actions.Dequeue(); - item(); - } - } + Signaled?.Invoke(null); + _signaled.WaitOne(); } } - public void Send(Action cb) - { - lock (_actions) - { - _actions.Enqueue(cb); - _queued.Set(); - } - } - class WatTimer : IDisposable + class TimerImpl : IDisposable { - private readonly IDisposable _timer; + private readonly DispatcherPriority _priority; + private readonly TimeSpan _interval; + private readonly Action _tick; + private Timer _timer; private GCHandle _handle; - public WatTimer(IDisposable timer) + public TimerImpl(DispatcherPriority priority, TimeSpan interval, Action tick) { - _timer = timer; + _priority = priority; + _interval = interval; + _tick = tick; + _timer = new Timer(OnTimer, null, interval, TimeSpan.FromMilliseconds(-1)); _handle = GCHandle.Alloc(_timer); } + private void OnTimer(object state) + { + if (_timer == null) + return; + Dispatcher.UIThread.Post(() => + { + + if (_timer == null) + return; + _tick(); + _timer?.Change(_interval, TimeSpan.FromMilliseconds(-1)); + }); + } + + public void Dispose() { _handle.Free(); _timer.Dispose(); + _timer = null; } } public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick) { - return new WatTimer(new System.Threading.Timer(delegate - { - var tcs = new TaskCompletionSource(); - Send(() => - { - try - { - tick(); - } - finally - { - tcs.SetResult(0); - } - }); - - - tcs.Task.Wait(); - }, null, TimeSpan.Zero, interval)); - - + return new TimerImpl(priority, interval, tick); } public void Signal(DispatcherPriority prio) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs index 3b6d071583..a7a94130ea 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs @@ -54,7 +54,7 @@ namespace Avalonia.DesignerSupport.Remote .Bind().ToConstant(instance) .Bind().ToConstant(threading) .Bind().ToConstant(new RenderLoop()) - .Bind().ToConstant(threading) + .Bind().ToConstant(new DefaultRenderTimer(60)) .Bind().ToSingleton() .Bind().ToConstant(instance) .Bind().ToSingleton() diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 9b9bd6c7b8..3376fa8a32 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -26,7 +26,7 @@ namespace Avalonia.LinuxFramebuffer public IRenderer CreateRenderer(IRenderRoot root) { - return new ImmediateRenderer(root); + return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); } public void Dispose() @@ -37,14 +37,6 @@ namespace Avalonia.LinuxFramebuffer public void Invalidate(Rect rect) { - if(_renderQueued) - return; - _renderQueued = true; - Dispatcher.UIThread.Post(() => - { - Paint?.Invoke(new Rect(default(Point), ClientSize)); - _renderQueued = false; - }); } public void SetInputRoot(IInputRoot inputRoot) @@ -62,7 +54,7 @@ namespace Avalonia.LinuxFramebuffer } public Size ClientSize => _fb.PixelSize; - public IMouseDevice MouseDevice => LinuxFramebufferPlatform.MouseDevice; + public IMouseDevice MouseDevice => new MouseDevice(); public double Scaling => 1; public IEnumerable Surfaces => new object[] {_fb}; public Action Input { get; set; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index ca31db6100..0589e30edb 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -40,7 +40,9 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput libinput_path_add_device(ctx, f); while (true) { + IntPtr ev; + libinput_dispatch(ctx); while ((ev = libinput_get_event(ctx)) != IntPtr.Zero) { @@ -50,25 +52,36 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput HandleTouch(ev, type); libinput_event_destroy(ev); + libinput_dispatch(ctx); } - libinput_dispatch(ctx); + + pollfd pfd = new pollfd {fd = fd, events = 1}; + NativeUnsafeMethods.poll(&pfd, new IntPtr(1), 10); } } private void ScheduleInput(RawInputEventArgs ev) { - _inputQueue.Enqueue(ev); - if (_inputQueue.Count == 1) + lock (_inputQueue) { - Dispatcher.UIThread.Post(() => + _inputQueue.Enqueue(ev); + if (_inputQueue.Count == 1) { - while (_inputQueue.Count > 0) + Dispatcher.UIThread.Post(() => { - Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); - var dequeuedEvent = _inputQueue.Dequeue(); - _onInput?.Invoke(dequeuedEvent); - } - }, DispatcherPriority.Input); + while (true) + { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); + RawInputEventArgs dequeuedEvent = null; + lock(_inputQueue) + if (_inputQueue.Count != 0) + dequeuedEvent = _inputQueue.Dequeue(); + if (dequeuedEvent == null) + return; + _onInput?.Invoke(dequeuedEvent); + } + }, DispatcherPriority.Input); + } } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 1eea1c07f7..85e3f97b74 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -18,8 +18,6 @@ namespace Avalonia.LinuxFramebuffer class LinuxFramebufferPlatform { LinuxFramebuffer _fb; - public static KeyboardDevice KeyboardDevice = new KeyboardDevice(); - public static MouseDevice MouseDevice = new MouseDevice(); private static readonly Stopwatch St = Stopwatch.StartNew(); internal static uint Timestamp => (uint)St.ElapsedTicks; public static InternalPlatformThreadingInterface Threading; @@ -33,13 +31,15 @@ namespace Avalonia.LinuxFramebuffer { Threading = new InternalPlatformThreadingInterface(); AvaloniaLocator.CurrentMutable + .Bind().ToConstant(Threading) + .Bind().ToConstant(new DefaultRenderTimer(60)) + .Bind().ToConstant(new RenderLoop()) .Bind().ToTransient() - .Bind().ToConstant(KeyboardDevice) + .Bind().ToConstant(new KeyboardDevice()) .Bind().ToSingleton() - .Bind().ToConstant(Threading) .Bind().ToConstant(new RenderLoop()) - .Bind().ToSingleton() - .Bind().ToConstant(Threading); + .Bind().ToSingleton(); + } internal static LinuxFramebufferLifetime Initialize(T builder, string fbdev = null) where T : AppBuilderBase, new() @@ -73,7 +73,9 @@ namespace Avalonia.LinuxFramebuffer var tl = new EmbeddableControlRoot(new FramebufferToplevelImpl(_fb, new LibInputBackend())); tl.Prepare(); _topLevel = tl; + _topLevel.Renderer.Start(); } + _topLevel.Content = value; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs index 8fbd78588f..18db176bcd 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs @@ -33,6 +33,10 @@ namespace Avalonia.LinuxFramebuffer [DllImport("libc", EntryPoint = "select", SetLastError = true)] public static extern int select(int nfds, void* rfds, void* wfds, void* exfds, IntPtr* timevals); + + [DllImport("libc", EntryPoint = "poll", SetLastError = true)] + public static extern int poll(pollfd* fds, IntPtr nfds, int timeout); + [DllImport("libevdev.so.2", EntryPoint = "libevdev_new_from_fd", SetLastError = true)] public static extern int libevdev_new_from_fd(int fd, out IntPtr dev); @@ -48,6 +52,13 @@ namespace Avalonia.LinuxFramebuffer public static extern input_absinfo* libevdev_get_abs_info(IntPtr dev, int code); } + [StructLayout(LayoutKind.Sequential)] + struct pollfd { + public int fd; /* file descriptor */ + public short events; /* requested events */ + public short revents; /* returned events */ + }; + enum FbIoCtl : uint { FBIOGET_VSCREENINFO = 0x4600, From 6f74b568b23ad604977ccb2c0b662a6123779c90 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 24 Jul 2019 01:01:22 -0800 Subject: [PATCH 7/9] Implemented libdrm backend with OpenGL acceleration support --- samples/ControlCatalog.NetCore/Program.cs | 21 +- src/Avalonia.OpenGL/EglContext.cs | 8 +- src/Avalonia.OpenGL/EglDisplay.cs | 94 +++--- src/Avalonia.OpenGL/EglInterface.cs | 5 + src/Avalonia.OpenGL/GlInterface.cs | 2 +- .../FramebufferToplevelImpl.cs | 18 +- .../LinuxFramebufferPlatform.cs | 29 +- .../Avalonia.LinuxFramebuffer/Output/Drm.cs | 292 ++++++++++++++++++ .../Output/DrmBindings.cs | 158 ++++++++++ .../Output/DrmOutput.cs | 250 +++++++++++++++ .../FbdevOutput.cs} | 11 +- .../Output/IOutputBackend.cs | 7 + 12 files changed, 828 insertions(+), 67 deletions(-) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs rename src/Linux/Avalonia.LinuxFramebuffer/{LinuxFramebuffer.cs => Output/FbdevOutput.cs} (92%) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 40321496c0..de9ca02ed1 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using Avalonia; using Avalonia.Controls; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Skia; using Avalonia.ReactiveUI; @@ -29,8 +30,13 @@ namespace ControlCatalog.NetCore var builder = BuildAvaloniaApp(); if (args.Contains("--fbdev")) { - System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); - return builder.StartLinuxFramebuffer(args); + SilenceConsole(); + return builder.StartLinuxFbDev(args); + } + else if (args.Contains("--drm")) + { + SilenceConsole(); + return builder.StartLinuxDrm(args); } else return builder.StartWithClassicDesktopLifetime(args); @@ -51,11 +57,14 @@ namespace ControlCatalog.NetCore .UseSkia() .UseReactiveUI(); - static void ConsoleSilencer() + static void SilenceConsole() { - Console.CursorVisible = false; - while (true) - Console.ReadKey(true); + new Thread(() => + { + Console.CursorVisible = false; + while (true) + Console.ReadKey(true); + }) {IsBackground = true}.Start(); } } } diff --git a/src/Avalonia.OpenGL/EglContext.cs b/src/Avalonia.OpenGL/EglContext.cs index 17caf84179..a39000f198 100644 --- a/src/Avalonia.OpenGL/EglContext.cs +++ b/src/Avalonia.OpenGL/EglContext.cs @@ -10,7 +10,7 @@ namespace Avalonia.OpenGL private readonly EglInterface _egl; private readonly object _lock = new object(); - public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, IntPtr offscreenSurface) + public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface) { _disp = display; _egl = egl; @@ -19,7 +19,7 @@ namespace Avalonia.OpenGL } public IntPtr Context { get; } - public IntPtr OffscreenSurface { get; } + public EglSurface OffscreenSurface { get; } public IGlDisplay Display => _disp; public IDisposable Lock() @@ -36,8 +36,8 @@ namespace Avalonia.OpenGL public void MakeCurrent(EglSurface surface) { - var surf = surface?.DangerousGetHandle() ?? OffscreenSurface; - if (!_egl.MakeCurrent(_disp.Handle, surf, surf, Context)) + var surf = surface ?? OffscreenSurface; + if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index b2b5a1a646..66418c0e15 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -12,49 +12,62 @@ namespace Avalonia.OpenGL private readonly IntPtr _display; private readonly IntPtr _config; private readonly int[] _contextAttributes; + private readonly int _surfaceType; public IntPtr Handle => _display; private AngleOptions.PlatformApi? _angleApi; - public EglDisplay(EglInterface egl) + + public EglDisplay(EglInterface egl) : this(egl, -1, IntPtr.Zero, null) + { + + } + public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) { - _egl = egl; + _egl = egl; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (platformType == -1 && platformDisplay == IntPtr.Zero) { - if (_egl.GetPlatformDisplayEXT == null) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); - - var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new List {AngleOptions.PlatformApi.DirectX9}; - - foreach (var platformApi in allowedApis) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - int dapi; - if (platformApi == AngleOptions.PlatformApi.DirectX9) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; - else if (platformApi == AngleOptions.PlatformApi.DirectX11) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; - else - continue; - - _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[] - { - EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE - }); - if (_display != IntPtr.Zero) + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); + + var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis + ?? new List {AngleOptions.PlatformApi.DirectX9}; + + foreach (var platformApi in allowedApis) { - _angleApi = platformApi; - break; + int dapi; + if (platformApi == AngleOptions.PlatformApi.DirectX9) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.DirectX11) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else + continue; + + _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] {EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE}); + if (_display != IntPtr.Zero) + { + _angleApi = platformApi; + break; + } } + + if (_display == IntPtr.Zero) + throw new OpenGlException("Unable to create ANGLE display"); } if (_display == IntPtr.Zero) - throw new OpenGlException("Unable to create ANGLE display"); + _display = _egl.GetDisplay(IntPtr.Zero); + } + else + { + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); + _display = _egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); } - if (_display == IntPtr.Zero) - _display = _egl.GetDisplay(IntPtr.Zero); - if (_display == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglGetDisplay", _egl); @@ -85,16 +98,14 @@ namespace Avalonia.OpenGL { if (!_egl.BindApi(cfg.Api)) continue; - + foreach(var surfaceType in new[]{EGL_PBUFFER_BIT|EGL_WINDOW_BIT, EGL_WINDOW_BIT}) foreach(var stencilSize in new[]{8, 1, 0}) foreach (var depthSize in new []{8, 1, 0}) { var attribs = new[] { - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - + EGL_SURFACE_TYPE, surfaceType, EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, - EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, @@ -108,6 +119,7 @@ namespace Avalonia.OpenGL if (numConfigs == 0) continue; _contextAttributes = cfg.Attributes; + _surfaceType = surfaceType; Type = cfg.Type; } } @@ -126,8 +138,10 @@ namespace Avalonia.OpenGL public GlDisplayType Type { get; } public GlInterface GlInterface { get; } public EglInterface EglInterface => _egl; - public IGlContext CreateContext(IGlContext share) + public EglContext CreateContext(IGlContext share) { + if((_surfaceType|EGL_PBUFFER_BIT) == 0) + throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces"); var shareCtx = (EglContext)share; var ctx = _egl.CreateContext(_display, _config, shareCtx?.Context ?? IntPtr.Zero, _contextAttributes); if (ctx == IntPtr.Zero) @@ -140,7 +154,17 @@ namespace Avalonia.OpenGL }); if (surf == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); - var rv = new EglContext(this, _egl, ctx, surf); + var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf)); + rv.MakeCurrent(null); + return rv; + } + + public EglContext CreateContext(EglContext share, EglSurface offscreenSurface) + { + var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes); + if (ctx == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateContext", _egl); + var rv = new EglContext(this, _egl, ctx, offscreenSurface); rv.MakeCurrent(null); return rv; } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 0a99778ddf..47088972a4 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -10,6 +10,11 @@ namespace Avalonia.OpenGL public EglInterface() : base(Load()) { + } + + public EglInterface(Func getProcAddress) : base(getProcAddress) + { + } public EglInterface(string library) : base(Load(library)) diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index f556949cfa..30f7d67152 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -39,7 +39,7 @@ namespace Avalonia.OpenGL [GlEntryPoint("glClearStencil")] public GlClearStencil ClearStencil { get; } - public delegate void GlClearColor(int r, int g, int b, int a); + public delegate void GlClearColor(float r, float g, float b, float a); [GlEntryPoint("glClearColor")] public GlClearColor ClearColor { get; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 3376fa8a32..5e2ba51caf 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.LinuxFramebuffer.Input; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -11,14 +12,14 @@ namespace Avalonia.LinuxFramebuffer { class FramebufferToplevelImpl : IEmbeddableWindowImpl, IScreenInfoProvider { - private readonly LinuxFramebuffer _fb; + private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; private bool _renderQueued; public IInputRoot InputRoot { get; private set; } - public FramebufferToplevelImpl(LinuxFramebuffer fb, IInputBackend inputBackend) + public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) { - _fb = fb; + _outputBackend = outputBackend; _inputBackend = inputBackend; Invalidate(default(Rect)); _inputBackend.Initialize(this, e => Input?.Invoke(e)); @@ -26,7 +27,10 @@ namespace Avalonia.LinuxFramebuffer public IRenderer CreateRenderer(IRenderRoot root) { - return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); + return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) + { + + }; } public void Dispose() @@ -53,10 +57,10 @@ namespace Avalonia.LinuxFramebuffer { } - public Size ClientSize => _fb.PixelSize; + public Size ClientSize => ScaledSize; public IMouseDevice MouseDevice => new MouseDevice(); public double Scaling => 1; - public IEnumerable Surfaces => new object[] {_fb}; + public IEnumerable Surfaces => new object[] {_outputBackend}; public Action Input { get; set; } public Action Paint { get; set; } public Action Resized { get; set; } @@ -68,6 +72,6 @@ namespace Avalonia.LinuxFramebuffer remove {} } - public Size ScaledSize => _fb.PixelSize / Scaling; + public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 85e3f97b74..2cc1f65202 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -9,6 +9,8 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.LinuxFramebuffer; using Avalonia.LinuxFramebuffer.Input.LibInput; +using Avalonia.LinuxFramebuffer.Output; +using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -17,19 +19,21 @@ namespace Avalonia.LinuxFramebuffer { class LinuxFramebufferPlatform { - LinuxFramebuffer _fb; + IOutputBackend _fb; private static readonly Stopwatch St = Stopwatch.StartNew(); internal static uint Timestamp => (uint)St.ElapsedTicks; public static InternalPlatformThreadingInterface Threading; - LinuxFramebufferPlatform(string fbdev = null) + LinuxFramebufferPlatform(IOutputBackend backend) { - _fb = new LinuxFramebuffer(fbdev); + _fb = backend; } void Initialize() { Threading = new InternalPlatformThreadingInterface(); + if (_fb is IWindowingPlatformGlFeature glFeature) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(glFeature); AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(60)) @@ -42,9 +46,10 @@ namespace Avalonia.LinuxFramebuffer } - internal static LinuxFramebufferLifetime Initialize(T builder, string fbdev = null) where T : AppBuilderBase, new() + + internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend) where T : AppBuilderBase, new() { - var platform = new LinuxFramebufferPlatform(fbdev); + var platform = new LinuxFramebufferPlatform(outputBackend); builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev"); return new LinuxFramebufferLifetime(platform._fb); } @@ -52,12 +57,12 @@ namespace Avalonia.LinuxFramebuffer class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime { - private readonly LinuxFramebuffer _fb; + private readonly IOutputBackend _fb; private TopLevel _topLevel; private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public CancellationToken Token => _cts.Token; - public LinuxFramebufferLifetime(LinuxFramebuffer fb) + public LinuxFramebufferLifetime(IOutputBackend fb) { _fb = fb; } @@ -102,10 +107,16 @@ namespace Avalonia.LinuxFramebuffer public static class LinuxFramebufferPlatformExtensions { - public static int StartLinuxFramebuffer(this T builder, string[] args, string fbdev = null) + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new FbdevOutput(fbdev)); + + public static int StartLinuxDrm(this T builder, string[] args, string card = null) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card)); + + public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend backend) where T : AppBuilderBase, new() { - var lifetime = LinuxFramebufferPlatform.Initialize(builder, fbdev); + var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend); builder.Instance.ApplicationLifetime = lifetime; builder.SetupWithoutStarting(); lifetime.Start(args); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs new file mode 100644 index 0000000000..e266c5ee54 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs @@ -0,0 +1,292 @@ +using System; +using System.Runtime.InteropServices; +// ReSharper disable FieldCanBeMadeReadOnly.Global +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable FieldCanBeMadeReadOnly.Local + +namespace Avalonia.LinuxFramebuffer.Output +{ + public enum DrmModeConnection + { + DRM_MODE_CONNECTED = 1, + DRM_MODE_DISCONNECTED = 2, + DRM_MODE_UNKNOWNCONNECTION = 3 + } + + public enum DrmModeSubPixel{ + DRM_MODE_SUBPIXEL_UNKNOWN = 1, + DRM_MODE_SUBPIXEL_HORIZONTAL_RGB = 2, + DRM_MODE_SUBPIXEL_HORIZONTAL_BGR = 3, + DRM_MODE_SUBPIXEL_VERTICAL_RGB = 4, + DRM_MODE_SUBPIXEL_VERTICAL_BGR = 5, + DRM_MODE_SUBPIXEL_NONE = 6 + } + + static unsafe class LibDrm + { + private const string libdrm = "libdrm.so.2"; + private const string libgbm = "libgbm.so.1"; + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void DrmEventVBlankHandlerDelegate(int fd, + uint sequence, + uint tv_sec, + uint tv_usec, + void* user_data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void DrmEventPageFlipHandlerDelegate(int fd, + uint sequence, + uint tv_sec, + uint tv_usec, + void* user_data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate IntPtr DrmEventPageFlipHandler2Delegate(int fd, + uint sequence, + uint tv_sec, + uint tv_usec, + uint crtc_id, + void* user_data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void DrmEventSequenceHandlerDelegate(int fd, + ulong sequence, + ulong ns, + ulong user_data); + + [StructLayout(LayoutKind.Sequential)] + public struct DrmEventContext + { + public int version; //4 + public IntPtr vblank_handler; + public IntPtr page_flip_handler; + public IntPtr page_flip_handler2; + public IntPtr sequence_handler; + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeRes { + + public int count_fbs; + public uint *fbs; + + public int count_crtcs; + public uint *crtcs; + + public int count_connectors; + public uint *connectors; + + public int count_encoders; + public uint *encoders; + + uint min_width, max_width; + uint min_height, max_height; + } + + [Flags] + public enum DrmModeType + { + DRM_MODE_TYPE_BUILTIN = (1 << 0), + DRM_MODE_TYPE_CLOCK_C = ((1 << 1) | DRM_MODE_TYPE_BUILTIN), + DRM_MODE_TYPE_CRTC_C = ((1 << 2) | DRM_MODE_TYPE_BUILTIN), + DRM_MODE_TYPE_PREFERRED = (1 << 3), + DRM_MODE_TYPE_DEFAULT = (1 << 4), + DRM_MODE_TYPE_USERDEF = (1 << 5), + DRM_MODE_TYPE_DRIVER = (1 << 6) + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeModeInfo + { + public uint clock; + public ushort hdisplay, hsync_start, hsync_end, htotal, hskew; + public ushort vdisplay, vsync_start, vsync_end, vtotal, vscan; + + public uint vrefresh; + + public uint flags; + public DrmModeType type; + public fixed byte name[32]; + public PixelSize Resolution => new PixelSize(hdisplay, vdisplay); + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeConnector { + public uint connector_id; + public uint encoder_id; /**< Encoder currently connected to */ + public uint connector_type; + public uint connector_type_id; + public DrmModeConnection connection; + public uint mmWidth, mmHeight; /**< HxW in millimeters */ + public DrmModeSubPixel subpixel; + + public int count_modes; + public drmModeModeInfo* modes; + + public int count_props; + public uint *props; /**< List of property ids */ + public ulong *prop_values; /**< List of property values */ + + public int count_encoders; + public uint *encoders; /**< List of encoder ids */ + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeEncoder { + public uint encoder_id; + public uint encoder_type; + public uint crtc_id; + public uint possible_crtcs; + public uint possible_clones; + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeCrtc { + public uint crtc_id; + public uint buffer_id; /**< FB id to connect to 0 = disconnect */ + + public uint x, y; /**< Position on the framebuffer */ + public uint width, height; + public int mode_valid; + public drmModeModeInfo mode; + + public int gamma_size; /**< Number of gamma stops */ + + } + + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeRes* drmModeGetResources(int fd); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeResources(drmModeRes* res); + + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeConnector* drmModeGetConnector(int fd, uint connector); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeConnector(drmModeConnector* res); + + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeEncoder* drmModeGetEncoder(int fd, uint id); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeEncoder(drmModeEncoder* enc); + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeCrtc* drmModeGetCrtc(int fd, uint id); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeCrtc(drmModeCrtc* enc); + + [DllImport(libdrm, SetLastError = true)] + public static extern int drmModeAddFB(int fd, uint width, uint height, byte depth, + byte bpp, uint pitch, uint bo_handle, + out uint buf_id); + [DllImport(libdrm, SetLastError = true)] + public static extern int drmModeSetCrtc(int fd, uint crtcId, uint bufferId, + uint x, uint y, uint *connectors, int count, + drmModeModeInfo* mode); + + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeRmFB(int fd, int id); + + [Flags] + public enum DrmModePageFlip + { + Event = 1, + Async = 2, + Absolute = 4, + Relative = 8, + } + + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModePageFlip(int fd, uint crtc_id, uint fb_id, + DrmModePageFlip flags, void *user_data); + + + [DllImport(libdrm, SetLastError = true)] + public static extern void drmHandleEvent(int fd, DrmEventContext* context); + + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_create_device(int fd); + + + [Flags] + public enum GbmBoFlags { + /** + * Buffer is going to be presented to the screen using an API such as KMS + */ + GBM_BO_USE_SCANOUT = (1 << 0), + /** + * Buffer is going to be used as cursor + */ + GBM_BO_USE_CURSOR = (1 << 1), + /** + * Deprecated + */ + GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR, + /** + * Buffer is to be used for rendering - for example it is going to be used + * as the storage for a color buffer + */ + GBM_BO_USE_RENDERING = (1 << 2), + /** + * Buffer can be used for gbm_bo_write. This is guaranteed to work + * with GBM_BO_USE_CURSOR, but may not work for other combinations. + */ + GBM_BO_USE_WRITE = (1 << 3), + /** + * Buffer is linear, i.e. not tiled. + */ + GBM_BO_USE_LINEAR = (1 << 4), + }; + + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_surface_create(IntPtr device, int width, int height, uint format, GbmBoFlags flags); + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_surface_lock_front_buffer(IntPtr surface); + [DllImport(libgbm, SetLastError = true)] + public static extern int gbm_surface_release_buffer(IntPtr surface, IntPtr bo); + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_bo_get_user_data(IntPtr surface); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void GbmBoUserDataDestroyCallbackDelegate(IntPtr bo, IntPtr data); + + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_bo_set_user_data(IntPtr bo, IntPtr userData, + GbmBoUserDataDestroyCallbackDelegate onFree); + + [DllImport(libgbm, SetLastError = true)] + public static extern uint gbm_bo_get_width(IntPtr bo); + + [DllImport(libgbm, SetLastError = true)] + public static extern uint gbm_bo_get_height(IntPtr bo); + + [DllImport(libgbm, SetLastError = true)] + public static extern uint gbm_bo_get_stride(IntPtr bo); + + + [StructLayout(LayoutKind.Explicit)] + public struct GbmBoHandle + { + [FieldOffset(0)] + public void *ptr; + [FieldOffset(0)] + public int s32; + [FieldOffset(0)] + public uint u32; + [FieldOffset(0)] + public long s64; + [FieldOffset(0)] + public ulong u64; + } + + [DllImport(libgbm, SetLastError = true)] + public static extern ulong gbm_bo_get_handle(IntPtr bo); + + public static class GbmColorFormats + { + public static uint FourCC(char a, char b, char c, char d) => + (uint)a | ((uint)b) << 8 | ((uint)c) << 16 | ((uint)d) << 24; + + public static uint GBM_FORMAT_XRGB8888 { get; } = FourCC('X', 'R', '2', '4'); + } + } + +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs new file mode 100644 index 0000000000..b5ebc4bcb7 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; +using static Avalonia.LinuxFramebuffer.Output.LibDrm; + +namespace Avalonia.LinuxFramebuffer.Output +{ + public unsafe class DrmConnector + { + private static string[] KnownConnectorTypes = + { + "None", "VGA", "DVI-I", "DVI-D", "DVI-A", "Composite", "S-Video", "LVDS", "Component", "DIN", + "DisplayPort", "HDMI-A", "HDMI-B", "TV", "eDP", "Virtual", "DSI" + }; + + public DrmModeConnection Connection { get; } + public uint Id { get; } + public string Name { get; } + public Size SizeMm { get; } + public DrmModeSubPixel SubPixel { get; } + internal uint EncoderId { get; } + internal List EncoderIds { get; } = new List(); + public List Modes { get; } = new List(); + internal DrmConnector(drmModeConnector* conn) + { + Connection = conn->connection; + Id = conn->connector_id; + SizeMm = new Size(conn->mmWidth, conn->mmHeight); + SubPixel = conn->subpixel; + for (var c = 0; c < conn->count_encoders;c++) + EncoderIds.Add(conn->encoders[c]); + EncoderId = conn->encoder_id; + for(var c=0; ccount_modes; c++) + Modes.Add(new DrmModeInfo(ref conn->modes[c])); + + if (conn->connector_type > KnownConnectorTypes.Length - 1) + Name = $"Unknown({conn->connector_type})-{conn->connector_type_id}"; + else + Name = KnownConnectorTypes[conn->connector_type] + "-" + conn->connector_type_id; + } + } + + public unsafe class DrmModeInfo + { + internal drmModeModeInfo Mode; + + internal DrmModeInfo(ref drmModeModeInfo info) + { + Mode = info; + fixed (void* pName = info.name) + Name = Marshal.PtrToStringAnsi(new IntPtr(pName)); + } + + public PixelSize Resolution => new PixelSize(Mode.hdisplay, Mode.vdisplay); + public bool IsPreferred => Mode.type.HasFlag(DrmModeType.DRM_MODE_TYPE_PREFERRED); + + public string Name { get; } + } + + unsafe class DrmEncoder + { + public drmModeEncoder Encoder { get; } + public List PossibleCrtcs { get; } = new List(); + + public DrmEncoder(drmModeEncoder encoder, drmModeCrtc[] crtcs) + { + Encoder = encoder; + for (var c = 0; c < crtcs.Length; c++) + { + var bit = 1 << c; + if ((encoder.possible_crtcs & bit) != 0) + PossibleCrtcs.Add(crtcs[c]); + } + } + } + + + + public unsafe class DrmResources + { + public List Connectors { get; }= new List(); + internal Dictionary Encoders { get; } = new Dictionary(); + public DrmResources(int fd) + { + var res = drmModeGetResources(fd); + if (res == null) + throw new Win32Exception("drmModeGetResources failed"); + + var crtcs = new drmModeCrtc[res->count_crtcs]; + for (var c = 0; c < res->count_crtcs; c++) + { + var crtc = drmModeGetCrtc(fd, res->crtcs[c]); + crtcs[c] = *crtc; + drmModeFreeCrtc(crtc); + } + + for (var c = 0; c < res->count_encoders; c++) + { + var enc = drmModeGetEncoder(fd, res->encoders[c]); + Encoders[res->encoders[c]] = new DrmEncoder(*enc, crtcs); + drmModeFreeEncoder(enc); + } + + for (var c = 0; c < res->count_connectors; c++) + { + var conn = drmModeGetConnector(fd, res->connectors[c]); + Connectors.Add(new DrmConnector(conn)); + drmModeFreeConnector(conn); + } + + + } + + public void Dump() + { + void Print(int off, string s) + { + for (var c = 0; c < off; c++) + Console.Write(" "); + Console.WriteLine(s); + } + Print(0, "Connectors"); + foreach (var conn in Connectors) + { + Print(1, $"{conn.Name}:"); + Print(2, $"Id: {conn.Id}"); + Print(2, $"Size: {conn.SizeMm} mm"); + Print(2, $"Encoder id: {conn.EncoderId}"); + Print(2, "Modes"); + foreach (var m in conn.Modes) + Print(3, $"{m.Name} {(m.IsPreferred ? "PREFERRED" : "")}"); + + + } + } + } + + public unsafe class DrmCard : IDisposable + { + public int Fd { get; private set; } + public DrmCard(string path = null) + { + path = path ?? "/dev/dri/card0"; + Fd = open(path, 2, 0); + if (Fd == -1) + throw new Win32Exception("Couldn't open " + path); + } + + public DrmResources GetResources() => new DrmResources(Fd); + public void Dispose() + { + close(Fd); + Fd = -1; + } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs new file mode 100644 index 0000000000..6a76977352 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.OpenGL; +using Avalonia.Platform.Interop; +using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; +using static Avalonia.LinuxFramebuffer.Output.LibDrm; +namespace Avalonia.LinuxFramebuffer.Output +{ + public unsafe class DrmOutput : IOutputBackend, IGlPlatformSurface, IWindowingPlatformGlFeature + { + private DrmCard _card; + private readonly EglGlPlatformSurface _eglPlatformSurface; + public PixelSize PixelSize => _mode.Resolution; + + public DrmOutput(string path = null) + { + var card = new DrmCard(path); + + var resources = card.GetResources(); + + + var connector = + resources.Connectors.FirstOrDefault(x => x.Connection == DrmModeConnection.DRM_MODE_CONNECTED); + if(connector == null) + throw new InvalidOperationException("Unable to find connected DRM connector"); + + var mode = connector.Modes.OrderByDescending(x => x.IsPreferred) + .ThenByDescending(x => x.Resolution.Width * x.Resolution.Height) + //.OrderByDescending(x => x.Resolution.Width * x.Resolution.Height) + .FirstOrDefault(); + if(mode == null) + throw new InvalidOperationException("Unable to find a usable DRM mode"); + Init(card, resources, connector, mode); + } + + public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo) + { + Init(card, resources, connector, modeInfo); + } + + [DllImport("libEGL.so.1")] + static extern IntPtr eglGetProcAddress(Utf8Buffer proc); + + private GbmBoUserDataDestroyCallbackDelegate FbDestroyDelegate; + private drmModeModeInfo _mode; + private EglDisplay _eglDisplay; + private EglSurface _eglSurface; + private EglContext _immediateContext; + private EglContext _deferredContext; + private IntPtr _currentBo; + private IntPtr _gbmTargetSurface; + private uint _crtcId; + + void FbDestroyCallback(IntPtr bo, IntPtr userData) + { + drmModeRmFB(_card.Fd, userData.ToInt32()); + } + + uint GetFbIdForBo(IntPtr bo) + { + if (bo == IntPtr.Zero) + throw new ArgumentException("bo is 0"); + var data = gbm_bo_get_user_data(bo); + if (data != IntPtr.Zero) + return (uint)data.ToInt32(); + + var w = gbm_bo_get_width(bo); + var h = gbm_bo_get_height(bo); + var stride = gbm_bo_get_stride(bo); + var handle = gbm_bo_get_handle(bo); + + var ret = drmModeAddFB(_card.Fd, w, h, 24, 32, stride, (uint)handle, out var fbHandle); + if (ret != 0) + throw new Win32Exception(ret, "drmModeAddFb failed"); + + gbm_bo_set_user_data(bo, new IntPtr((int)fbHandle), FbDestroyDelegate); + + + return fbHandle; + } + + + void Init(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo) + { + FbDestroyDelegate = FbDestroyCallback; + _card = card; + uint GetCrtc() + { + if (resources.Encoders.TryGetValue(connector.EncoderId, out var encoder)) + { + // Not sure why that should work + return encoder.Encoder.crtc_id; + } + else + { + foreach (var encId in connector.EncoderIds) + { + if (resources.Encoders.TryGetValue(encId, out encoder) + && encoder.PossibleCrtcs.Count>0) + return encoder.PossibleCrtcs.First().crtc_id; + } + + throw new InvalidOperationException("Unable to find CRTC matching the desired mode"); + } + } + + _crtcId = GetCrtc(); + var device = gbm_create_device(card.Fd); + _gbmTargetSurface = gbm_surface_create(device, modeInfo.Resolution.Width, modeInfo.Resolution.Height, + GbmColorFormats.GBM_FORMAT_XRGB8888, GbmBoFlags.GBM_BO_USE_SCANOUT | GbmBoFlags.GBM_BO_USE_RENDERING); + if(_gbmTargetSurface == null) + throw new InvalidOperationException("Unable to create GBM surface"); + + + + _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), 0x31D7, device, null); + _eglSurface = _eglDisplay.CreateWindowSurface(_gbmTargetSurface); + + + EglContext CreateContext(EglContext share) + { + var offSurf = gbm_surface_create(device, 1, 1, GbmColorFormats.GBM_FORMAT_XRGB8888, + GbmBoFlags.GBM_BO_USE_RENDERING); + if (offSurf == null) + throw new InvalidOperationException("Unable to create 1x1 sized GBM surface"); + return _eglDisplay.CreateContext(share, _eglDisplay.CreateWindowSurface(offSurf)); + } + + _immediateContext = CreateContext(null); + _deferredContext = CreateContext(_immediateContext); + + _immediateContext.MakeCurrent(_eglSurface); + _eglDisplay.GlInterface.ClearColor(0, 0, 0, 0); + _eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT); + _eglSurface.SwapBuffers(); + var bo = gbm_surface_lock_front_buffer(_gbmTargetSurface); + var fbId = GetFbIdForBo(bo); + var connectorId = connector.Id; + var mode = modeInfo.Mode; + + + var res = drmModeSetCrtc(_card.Fd, _crtcId, fbId, 0, 0, &connectorId, 1, &mode); + if (res != 0) + throw new Win32Exception(res, "drmModeSetCrtc failed"); + + _mode = mode; + _currentBo = bo; + + // Go trough two cycles of buffer swapping (there are render artifacts otherwise) + for(var c=0;c<2;c++) + using (CreateGlRenderTarget().BeginDraw()) + { + _eglDisplay.GlInterface.ClearColor(0, 0, 0, 0); + _eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT); + } + } + + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + { + return new RenderTarget(this); + } + + class RenderTarget : IGlPlatformSurfaceRenderTarget + { + private readonly DrmOutput _parent; + + public RenderTarget(DrmOutput parent) + { + _parent = parent; + } + public void Dispose() + { + // We are wrapping GBM buffer chain associated with CRTC, and don't free it on a whim + } + + class RenderSession : IGlPlatformSurfaceRenderingSession + { + private readonly DrmOutput _parent; + + public RenderSession(DrmOutput parent) + { + _parent = parent; + } + + public void Dispose() + { + _parent._eglDisplay.GlInterface.Flush(); + _parent._eglSurface.SwapBuffers(); + + var nextBo = gbm_surface_lock_front_buffer(_parent._gbmTargetSurface); + if (nextBo == IntPtr.Zero) + { + // Not sure what else can be done + Console.WriteLine("gbm_surface_lock_front_buffer failed"); + } + else + { + + var fb = _parent.GetFbIdForBo(nextBo); + bool waitingForFlip = true; + + drmModePageFlip(_parent._card.Fd, _parent._crtcId, fb, DrmModePageFlip.Event, null); + + DrmEventPageFlipHandlerDelegate flipCb = + (int fd, uint sequence, uint tv_sec, uint tv_usec, void* user_data) => + { + waitingForFlip = false; + }; + var cbHandle = GCHandle.Alloc(flipCb); + var ctx = new DrmEventContext + { + version = 4, page_flip_handler = Marshal.GetFunctionPointerForDelegate(flipCb) + }; + while (waitingForFlip) + { + var pfd = new pollfd {events = 1, fd = _parent._card.Fd}; + poll(&pfd, new IntPtr(1), -1); + drmHandleEvent(_parent._card.Fd, &ctx); + } + + cbHandle.Free(); + gbm_surface_release_buffer(_parent._gbmTargetSurface, _parent._currentBo); + _parent._currentBo = nextBo; + } + _parent._eglDisplay.ClearContext(); + } + + + public IGlDisplay Display => _parent._eglDisplay; + + public PixelSize Size => _parent._mode.Resolution; + + public double Scaling => 1; + } + + public IGlPlatformSurfaceRenderingSession BeginDraw() + { + _parent._deferredContext.MakeCurrent(_parent._eglSurface); + return new RenderSession(_parent); + } + } + + IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext; + } + + +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs similarity index 92% rename from src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs rename to src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 1e25bd4a8a..3021c29015 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -2,11 +2,12 @@ using System.Runtime.InteropServices; using System.Text; using Avalonia.Controls.Platform.Surfaces; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; namespace Avalonia.LinuxFramebuffer { - public sealed unsafe class LinuxFramebuffer : IFramebufferPlatformSurface, IDisposable + public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend { private readonly Vector _dpi; private int _fd; @@ -15,7 +16,7 @@ namespace Avalonia.LinuxFramebuffer private IntPtr _mappedLength; private IntPtr _mappedAddress; - public LinuxFramebuffer(string fileName = null, Vector? dpi = null) + public FbdevOutput(string fileName = null, Vector? dpi = null) { _dpi = dpi ?? new Vector(96, 96); fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; @@ -85,14 +86,14 @@ namespace Avalonia.LinuxFramebuffer public string Id { get; private set; } - public Size PixelSize + public PixelSize PixelSize { get { fb_var_screeninfo nfo; if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, &nfo)) throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); - return new Size(nfo.xres, nfo.yres); + return new PixelSize((int)nfo.xres, (int)nfo.yres); } } @@ -123,7 +124,7 @@ namespace Avalonia.LinuxFramebuffer GC.SuppressFinalize(this); } - ~LinuxFramebuffer() + ~FbdevOutput() { ReleaseUnmanagedResources(); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs new file mode 100644 index 0000000000..01690f07ac --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs @@ -0,0 +1,7 @@ +namespace Avalonia.LinuxFramebuffer.Output +{ + public interface IOutputBackend + { + PixelSize PixelSize { get; } + } +} From 567defb825167d29e2bcd5cd0d9be9a3b4ab6a3c Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 24 Jul 2019 01:39:57 -0800 Subject: [PATCH 8/9] Basic support for absolute pointing libinput devices --- .../Input/LibInput/LibInputBackend.cs | 45 +++++++++++++++++++ .../LibInput/LibInputNativeUnsafeMethods.cs | 21 ++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs index 0589e30edb..723028c666 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -15,6 +15,8 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput private IInputRoot _inputRoot; private readonly Queue _inputThreadActions = new Queue(); private TouchDevice _touch = new TouchDevice(); + private MouseDevice _mouse = new MouseDevice(); + private Point _mousePosition; private readonly Queue _inputQueue = new Queue(); private Action _onInput; @@ -50,6 +52,10 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput if (type >= LibInputEventType.LIBINPUT_EVENT_TOUCH_DOWN && type <= LibInputEventType.LIBINPUT_EVENT_TOUCH_CANCEL) HandleTouch(ev, type); + + if (type >= LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION + && type <= LibInputEventType.LIBINPUT_EVENT_POINTER_AXIS) + HandlePointer(ev, type); libinput_event_destroy(ev); libinput_dispatch(ctx); @@ -123,6 +129,45 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput } } + private void HandlePointer(IntPtr ev, LibInputEventType type) + { + //TODO: support input modifiers + var pev = libinput_event_get_pointer_event(ev); + var info = _screen.ScaledSize; + var ts = libinput_event_pointer_get_time_usec(pev) / 1000; + if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION_ABSOLUTE) + { + _mousePosition = new Point(libinput_event_pointer_get_absolute_x_transformed(pev, (int)info.Width), + libinput_event_pointer_get_absolute_y_transformed(pev, (int)info.Height)); + ScheduleInput(new RawPointerEventArgs(_mouse, ts, _inputRoot, RawPointerEventType.Move, _mousePosition, + InputModifiers.None)); + } + else if (type == LibInputEventType.LIBINPUT_EVENT_POINTER_BUTTON) + { + var button = (EvKey)libinput_event_pointer_get_button(pev); + var buttonState = libinput_event_pointer_get_button_state(pev); + + + var evnt = button == EvKey.BTN_LEFT ? + (buttonState == 1 ? RawPointerEventType.LeftButtonDown : RawPointerEventType.LeftButtonUp) : + button == EvKey.BTN_MIDDLE ? + (buttonState == 1 ? RawPointerEventType.MiddleButtonDown : RawPointerEventType.MiddleButtonUp) : + button == EvKey.BTN_RIGHT ? + (buttonState == 1 ? + RawPointerEventType.RightButtonDown : + RawPointerEventType.RightButtonUp) : + (RawPointerEventType)(-1); + if (evnt == (RawPointerEventType)(-1)) + return; + + + ScheduleInput( + new RawPointerEventArgs(_mouse, ts, _inputRoot, evnt, _mousePosition, InputModifiers.None)); + } + + } + + public void Initialize(IScreenInfoProvider screen, Action onInput) { diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs index 6c82f53071..0492090461 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs @@ -116,7 +116,24 @@ namespace Avalonia.LinuxFramebuffer.Input.LibInput [DllImport(LibInput)] public extern static double libinput_event_touch_get_y_transformed(IntPtr ev, int height); - - + + [DllImport(LibInput)] + public extern static IntPtr libinput_event_get_pointer_event(IntPtr ev); + + + [DllImport(LibInput)] + public extern static ulong libinput_event_pointer_get_time_usec(IntPtr ev); + + [DllImport(LibInput)] + public extern static double libinput_event_pointer_get_absolute_x_transformed(IntPtr ev, int width); + + [DllImport(LibInput)] + public extern static double libinput_event_pointer_get_absolute_y_transformed(IntPtr ev, int height); + + [DllImport(LibInput)] + public extern static int libinput_event_pointer_get_button(IntPtr ev); + + [DllImport(LibInput)] + public extern static int libinput_event_pointer_get_button_state(IntPtr ev); } } From 53d060d707f3e66d881ea9fa34fc4082e235a0f8 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 24 Jul 2019 13:06:33 +0300 Subject: [PATCH 9/9] Bump skia linux binaries version --- build/SkiaSharp.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index cf8e0fd13a..c03ad0fefd 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - +