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.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/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/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/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..5e2ba51caf 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -2,30 +2,35 @@ 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; namespace Avalonia.LinuxFramebuffer { - class FramebufferToplevelImpl : IEmbeddableWindowImpl + 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) + public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) { - _fb = fb; + _outputBackend = outputBackend; + _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) { - return new ImmediateRenderer(root); + return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) + { + + }; } public void Dispose() @@ -36,19 +41,12 @@ 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) { InputRoot = inputRoot; + _inputBackend.SetInputRoot(inputRoot); } public Point PointToClient(PixelPoint p) => p.ToPoint(1); @@ -59,10 +57,10 @@ namespace Avalonia.LinuxFramebuffer { } - public Size ClientSize => _fb.PixelSize; - public IMouseDevice MouseDevice => LinuxFramebufferPlatform.MouseDevice; + 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; } @@ -73,5 +71,7 @@ namespace Avalonia.LinuxFramebuffer add {} remove {} } + + public Size ScaledSize => _outputBackend.PixelSize.ToSize(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..723028c666 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputBackend.cs @@ -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 _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; + 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; + 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 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..0492090461 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Input/LibInput/LibInputNativeUnsafeMethods.cs @@ -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 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); + + [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); + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 396942c8dd..2cc1f65202 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -8,6 +8,9 @@ using Avalonia.Controls.Platform; 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; @@ -16,34 +19,37 @@ namespace Avalonia.LinuxFramebuffer { class LinuxFramebufferPlatform { - LinuxFramebuffer _fb; - public static KeyboardDevice KeyboardDevice = new KeyboardDevice(); - public static MouseDevice MouseDevice = new MouseDevice(); + 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)) + .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() + + 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); } @@ -51,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; } @@ -69,10 +75,12 @@ 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; + _topLevel.Renderer.Start(); } + _topLevel.Content = value; } } @@ -99,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/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..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, @@ -188,7 +199,7 @@ namespace Avalonia.LinuxFramebuffer unsafe struct fd_set { public int count; - public fixed int fds [256]; + public fixed byte fds [256]; } enum AxisEventCode 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; } + } +}