committed by
GitHub
34 changed files with 1600 additions and 686 deletions
@ -1,6 +1,6 @@ |
|||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> |
|||
<ItemGroup> |
|||
<PackageReference Include="SkiaSharp" Version="1.68.0" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.1" /> |
|||
<PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="Avalonia.Skia.Linux.Natives" Version="1.68.0.2" /> |
|||
</ItemGroup> |
|||
</Project> |
|||
|
|||
@ -0,0 +1,150 @@ |
|||
// 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 |
|||
{ |
|||
/// <summary>
|
|||
/// Stores values with <see cref="AvaloniaProperty"/> as key.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">Stored value type.</typeparam>
|
|||
internal sealed class AvaloniaPropertyValueStore<TValue> |
|||
{ |
|||
private Entry[] _entries; |
|||
|
|||
public AvaloniaPropertyValueStore() |
|||
{ |
|||
// 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 AddValue(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 SetValue(AvaloniaProperty property, TValue value) |
|||
{ |
|||
_entries[TryFindEntry(property.Id).Item1].Value = value; |
|||
} |
|||
|
|||
public Dictionary<AvaloniaProperty, TValue> ToDictionary() |
|||
{ |
|||
var dict = new Dictionary<AvaloniaProperty, TValue>(_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; |
|||
} |
|||
} |
|||
} |
|||
@ -1,168 +1,122 @@ |
|||
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 |
|||
{ |
|||
/// <summary>
|
|||
/// Callback invoked when deferred setter wants to set a value.
|
|||
/// </summary>
|
|||
/// <typeparam name="TValue">Value type.</typeparam>
|
|||
/// <param name="property">Property being set.</param>
|
|||
/// <param name="backing">Backing field reference.</param>
|
|||
/// <param name="value">New value.</param>
|
|||
internal delegate void SetAndNotifyCallback<TValue>(AvaloniaProperty property, ref TValue backing, TValue value); |
|||
|
|||
/// <summary>
|
|||
/// A utility class to enable deferring assignment until after property-changed notifications are sent.
|
|||
/// Used to fix #855.
|
|||
/// </summary>
|
|||
/// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
|
|||
class DeferredSetter<TSetRecord> |
|||
internal sealed class DeferredSetter<TSetRecord> |
|||
{ |
|||
private struct NotifyDisposable : IDisposable |
|||
private readonly SingleOrQueue<TSetRecord> _pendingValues; |
|||
private bool _isNotifying; |
|||
|
|||
public DeferredSetter() |
|||
{ |
|||
private readonly SettingStatus status; |
|||
_pendingValues = new SingleOrQueue<TSetRecord>(); |
|||
} |
|||
|
|||
internal NotifyDisposable(SettingStatus status) |
|||
{ |
|||
this.status = status; |
|||
status.Notifying = true; |
|||
} |
|||
private static void SetAndRaisePropertyChanged(AvaloniaObject source, AvaloniaProperty<TSetRecord> property, ref TSetRecord backing, TSetRecord value) |
|||
{ |
|||
var old = backing; |
|||
|
|||
public void Dispose() |
|||
{ |
|||
status.Notifying = false; |
|||
} |
|||
backing = value; |
|||
|
|||
source.RaisePropertyChanged(property, old, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Information on current setting/notification status of a property.
|
|||
/// </summary>
|
|||
private class SettingStatus |
|||
public bool SetAndNotify( |
|||
AvaloniaObject source, |
|||
AvaloniaProperty<TSetRecord> property, |
|||
ref TSetRecord backing, |
|||
TSetRecord value) |
|||
{ |
|||
public bool Notifying { get; set; } |
|||
|
|||
private SingleOrQueue<TSetRecord> pendingValues; |
|||
|
|||
public SingleOrQueue<TSetRecord> PendingValues |
|||
if (!_isNotifying) |
|||
{ |
|||
get |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>()); |
|||
SetAndRaisePropertyChanged(source, property, ref backing, value); |
|||
} |
|||
} |
|||
} |
|||
|
|||
private Dictionary<AvaloniaProperty, SettingStatus> _setRecords; |
|||
private Dictionary<AvaloniaProperty, SettingStatus> SetRecords |
|||
=> _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>()); |
|||
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; |
|||
_pendingValues.Enqueue(value); |
|||
|
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Mark the property as currently notifying.
|
|||
/// </summary>
|
|||
/// <param name="property">The property to mark as notifying.</param>
|
|||
/// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
|
|||
private NotifyDisposable MarkNotifying(AvaloniaProperty property) |
|||
public bool SetAndNotifyCallback<TValue>(AvaloniaProperty property, SetAndNotifyCallback<TValue> setAndNotifyCallback, ref TValue backing, TValue value) |
|||
where TValue : TSetRecord |
|||
{ |
|||
Contract.Requires<InvalidOperationException>(!IsNotifying(property)); |
|||
|
|||
SettingStatus status = GetOrCreateStatus(property); |
|||
|
|||
return new NotifyDisposable(status); |
|||
} |
|||
if (!_isNotifying) |
|||
{ |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
setAndNotifyCallback(property, ref backing, value); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Check if the property is currently notifying listeners.
|
|||
/// </summary>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <returns>If the property is currently notifying listeners.</returns>
|
|||
private bool IsNotifying(AvaloniaProperty property) |
|||
=> SetRecords.TryGetValue(property, out var value) && value.Notifying; |
|||
if (!_pendingValues.Empty) |
|||
{ |
|||
using (new NotifyDisposable(this)) |
|||
{ |
|||
while (!_pendingValues.Empty) |
|||
{ |
|||
setAndNotifyCallback(property, ref backing, (TValue) _pendingValues.Dequeue()); |
|||
} |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Add a pending assignment for the property.
|
|||
/// </summary>
|
|||
/// <param name="property">The property.</param>
|
|||
/// <param name="value">The value to assign.</param>
|
|||
private void AddPendingSet(AvaloniaProperty property, TSetRecord value) |
|||
{ |
|||
Contract.Requires<InvalidOperationException>(IsNotifying(property)); |
|||
return true; |
|||
} |
|||
|
|||
GetOrCreateStatus(property).PendingValues.Enqueue(value); |
|||
} |
|||
_pendingValues.Enqueue(value); |
|||
|
|||
/// <summary>
|
|||
/// Checks if there are any pending assignments for the property.
|
|||
/// </summary>
|
|||
/// <param name="property">The property to check.</param>
|
|||
/// <returns>If the property has any pending assignments.</returns>
|
|||
private bool HasPendingSet(AvaloniaProperty property) |
|||
{ |
|||
return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty; |
|||
return false; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Gets the first pending assignment for the property.
|
|||
/// Disposable that marks the property as currently notifying.
|
|||
/// When disposed, marks the property as done notifying.
|
|||
/// </summary>
|
|||
/// <param name="property">The property to check.</param>
|
|||
/// <returns>The first pending assignment for the property.</returns>
|
|||
private TSetRecord GetFirstPendingSet(AvaloniaProperty property) |
|||
private readonly struct NotifyDisposable : IDisposable |
|||
{ |
|||
return GetOrCreateStatus(property).PendingValues.Dequeue(); |
|||
} |
|||
|
|||
public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback); |
|||
private readonly DeferredSetter<TSetRecord> _setter; |
|||
|
|||
/// <summary>
|
|||
/// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824
|
|||
/// </summary>
|
|||
/// <param name="property">The property to set.</param>
|
|||
/// <param name="backing">The backing field for the property</param>
|
|||
/// <param name="setterCallback">
|
|||
/// 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.
|
|||
/// </param>
|
|||
/// <param name="value">The value to try to set.</param>
|
|||
public bool SetAndNotify<TValue>( |
|||
AvaloniaProperty property, |
|||
ref TValue backing, |
|||
SetterDelegate<TValue> setterCallback, |
|||
TSetRecord value) |
|||
{ |
|||
Contract.Requires<ArgumentNullException>(setterCallback != null); |
|||
if (!IsNotifying(property)) |
|||
internal NotifyDisposable(DeferredSetter<TSetRecord> 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; |
|||
} |
|||
} |
|||
} |
|||
|
|||
@ -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<List<EvDevDevice>> AllMouseDevices = new Lazy<List<EvDevDevice>>(() |
|||
=> OpenMouseDevices()); |
|||
|
|||
private static List<EvDevDevice> OpenMouseDevices() |
|||
{ |
|||
var rv = new List<EvDevDevice>(); |
|||
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<EvDevDevice> MouseDevices => AllMouseDevices.Value; |
|||
|
|||
|
|||
public int Fd { get; } |
|||
private IntPtr _dev; |
|||
public string Name { get; } |
|||
public List<EvType> EventTypes { get; private set; } = new List<EvType>(); |
|||
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; } |
|||
} |
|||
} |
|||
@ -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<RawInputEventArgs> onInput); |
|||
void SetInputRoot(IInputRoot root); |
|||
} |
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Avalonia.LinuxFramebuffer.Input |
|||
{ |
|||
public interface IScreenInfoProvider |
|||
{ |
|||
Size ScaledSize { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,183 @@ |
|||
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<Action> _inputThreadActions = new Queue<Action>(); |
|||
private TouchDevice _touch = new TouchDevice(); |
|||
private MouseDevice _mouse = new MouseDevice(); |
|||
private Point _mousePosition; |
|||
|
|||
private readonly Queue<RawInputEventArgs> _inputQueue = new Queue<RawInputEventArgs>(); |
|||
private Action<RawInputEventArgs> _onInput; |
|||
private Dictionary<int, Point> _pointers = new Dictionary<int, Point>(); |
|||
|
|||
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; |
|||
libinput_dispatch(ctx); |
|||
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); |
|||
|
|||
if (type >= LibInputEventType.LIBINPUT_EVENT_POINTER_MOTION |
|||
&& type <= LibInputEventType.LIBINPUT_EVENT_POINTER_AXIS) |
|||
HandlePointer(ev, type); |
|||
|
|||
libinput_event_destroy(ev); |
|||
libinput_dispatch(ctx); |
|||
} |
|||
|
|||
pollfd pfd = new pollfd {fd = fd, events = 1}; |
|||
NativeUnsafeMethods.poll(&pfd, new IntPtr(1), 10); |
|||
} |
|||
} |
|||
|
|||
private void ScheduleInput(RawInputEventArgs ev) |
|||
{ |
|||
lock (_inputQueue) |
|||
{ |
|||
_inputQueue.Enqueue(ev); |
|||
if (_inputQueue.Count == 1) |
|||
{ |
|||
Dispatcher.UIThread.Post(() => |
|||
{ |
|||
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); |
|||
} |
|||
} |
|||
} |
|||
|
|||
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)); |
|||
} |
|||
} |
|||
|
|||
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<RawInputEventArgs> onInput) |
|||
{ |
|||
_screen = screen; |
|||
_onInput = onInput; |
|||
} |
|||
|
|||
public void SetInputRoot(IInputRoot root) |
|||
{ |
|||
_inputRoot = root; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,139 @@ |
|||
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>(TDelegate del) |
|||
{ |
|||
GCHandle.Alloc(del); |
|||
return Marshal.GetFunctionPointerForDelegate(del); |
|||
} |
|||
|
|||
s_Interface[0] = Convert<OpenRestrictedCallbackDelegate>(OpenRestricted); |
|||
s_Interface[1] = Convert<CloseRestrictedCallbackDelegate>(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); |
|||
|
|||
[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); |
|||
} |
|||
} |
|||
@ -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<RawInputEventArgs> 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))); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -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'); |
|||
} |
|||
} |
|||
|
|||
} |
|||
@ -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<uint> EncoderIds { get; } = new List<uint>(); |
|||
public List<DrmModeInfo> Modes { get; } = new List<DrmModeInfo>(); |
|||
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; c<conn->count_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<drmModeCrtc> PossibleCrtcs { get; } = new List<drmModeCrtc>(); |
|||
|
|||
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<DrmConnector> Connectors { get; }= new List<DrmConnector>(); |
|||
internal Dictionary<uint, DrmEncoder> Encoders { get; } = new Dictionary<uint, DrmEncoder>(); |
|||
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; |
|||
} |
|||
} |
|||
} |
|||
@ -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; |
|||
} |
|||
|
|||
|
|||
} |
|||
@ -0,0 +1,7 @@ |
|||
namespace Avalonia.LinuxFramebuffer.Output |
|||
{ |
|||
public interface IOutputBackend |
|||
{ |
|||
PixelSize PixelSize { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,65 @@ |
|||
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 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<DirectClass, int> IntValueProperty = |
|||
AvaloniaProperty.RegisterDirect<DirectClass, int>(nameof(IntValue), |
|||
o => o.IntValue, |
|||
(o, v) => o.IntValue = v); |
|||
|
|||
public int IntValue |
|||
{ |
|||
get => _intValue; |
|||
set => SetAndRaise(IntValueProperty, ref _intValue, value); |
|||
} |
|||
|
|||
public int IntValueSimple |
|||
{ |
|||
get => _intValue; |
|||
set |
|||
{ |
|||
VerifyAccess(); |
|||
|
|||
if (_intValue == value) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var old = _intValue; |
|||
_intValue = value; |
|||
|
|||
RaisePropertyChanged(IntValueProperty, old, _intValue); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue