From 70abfca7d059d0270adaec31dde2ae761e28b340 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 22 Mar 2017 14:52:51 +0300 Subject: [PATCH] Implemented support for running on top of fbdev and libevdev2 --- Avalonia.sln | 47 +++- .../ControlCatalog.NetCore.csproj | 1 + samples/ControlCatalog.NetCore/Program.cs | 11 +- .../Avalonia.LinuxFramebuffer.csproj | 14 + .../Avalonia.LinuxFramebuffer/EvDevDevice.cs | 89 ++++++ .../FramebufferToplevelImpl.cs | 68 +++++ .../LinuxFramebuffer.cs | 138 ++++++++++ .../LinuxFramebufferPlatform.cs | 75 ++++++ .../LockedFramebuffer.cs | 47 ++++ src/Linux/Avalonia.LinuxFramebuffer/Mice.cs | 120 +++++++++ .../NativeUnsafeMethods.cs | 254 ++++++++++++++++++ .../PlatformThreadingInterface.cs | 112 ++++++++ src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs | 21 ++ 13 files changed, 993 insertions(+), 4 deletions(-) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Mice.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs diff --git a/Avalonia.sln b/Avalonia.sln index 3fb5ec2693..cc166bc495 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26228.9 +VisualStudioVersion = 15.0.26228.4 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Base", "src\Avalonia.Base\Avalonia.Base.csproj", "{B09B78D8-9B26-48B0-9149-D64A2F120F3F}" EndProject @@ -185,6 +185,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6F build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.LinuxFramebuffer", "src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj", "{854568D5-13D1-4B4F-B50D-534DC7EFD3C9}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Skia\Avalonia.Skia\Avalonia.Skia.projitems*{2f59f3d0-748d-4652-b01e-e0d954756308}*SharedItemsImports = 13 @@ -2503,6 +2507,46 @@ Global {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3}.Release|Mono.Build.0 = Release|Any CPU {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3}.Release|x86.ActiveCfg = Release|Any CPU {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3}.Release|x86.Build.0 = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|Mono.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|Mono.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|x86.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Ad-Hoc|x86.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|iPhone.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|Mono.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|Mono.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|x86.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.AppStore|x86.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|iPhone.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Mono.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|Mono.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|x86.ActiveCfg = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Debug|x86.Build.0 = Debug|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Any CPU.Build.0 = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|iPhone.ActiveCfg = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|iPhone.Build.0 = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Mono.ActiveCfg = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|Mono.Build.0 = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|x86.ActiveCfg = Release|Any CPU + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2561,5 +2605,6 @@ Global {39D7B147-1A5B-47C2-9D01-21FB7C47C4B3} = {9B9E3891-2366-4253-A952-D08BCEB71098} {F3AC8BC1-27F5-4255-9AFC-04ABFD11683A} = {74487168-7D91-487E-BF93-055F2251461E} {4D6FAF79-58B4-482F-9122-0668C346364C} = {74487168-7D91-487E-BF93-055F2251461E} + {854568D5-13D1-4B4F-B50D-534DC7EFD3C9} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} EndGlobalSection EndGlobal diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj index d43c3a060e..e0e848b91b 100644 --- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj +++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj @@ -7,6 +7,7 @@ + diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 57a508f923..f1146de013 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Avalonia; namespace ControlCatalog.NetCore @@ -7,9 +8,13 @@ namespace ControlCatalog.NetCore { static void Main(string[] args) { - AppBuilder.Configure() - .UsePlatformDetect() - .Start(); + if (args.Contains("--fbdev")) + AppBuilder.Configure() + .InitializeWithLinuxFramebuffer(tl => tl.Content = new MainView()); + else + AppBuilder.Configure() + .UsePlatformDetect() + .Start(); } } } \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj new file mode 100644 index 0000000000..3a3e08135d --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Avalonia.LinuxFramebuffer.csproj @@ -0,0 +1,14 @@ + + + netstandard1.3 + true + + + + + + + + + + \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs b/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs new file mode 100644 index 0000000000..7b399863f7 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/EvDevDevice.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; + +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 new file mode 100644 index 0000000000..193d2c1d05 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Platform; +using Avalonia.Threading; + +namespace Avalonia.LinuxFramebuffer +{ + class FramebufferToplevelImpl : IEmbeddableWindowImpl + { + private readonly LinuxFramebuffer _fb; + private bool _renderQueued; + public IInputRoot InputRoot { get; private set; } + + public FramebufferToplevelImpl(LinuxFramebuffer fb) + { + _fb = fb; + Invalidate(default(Rect)); + var mice = new Mice(ClientSize.Width, ClientSize.Height); + mice.Start(); + mice.Event += e => Input?.Invoke(e); + } + + public void Dispose() + { + throw new NotSupportedException(); + } + + + public void Invalidate(Rect rect) + { + if(_renderQueued) + return; + _renderQueued = true; + Dispatcher.UIThread.InvokeAsync(() => + { + Paint?.Invoke(new Rect(default(Point), ClientSize)); + _renderQueued = false; + }); + } + + public void SetInputRoot(IInputRoot inputRoot) + { + InputRoot = inputRoot; + } + + public Point PointToClient(Point point) => point; + + public Point PointToScreen(Point point) => point; + + public void SetCursor(IPlatformHandle cursor) + { + } + + public Size ClientSize => _fb.PixelSize; + public double Scaling => 1; + public IEnumerable Surfaces => new object[] {_fb}; + public Action Input { get; set; } + public Action Paint { get; set; } + public Action Resized { get; set; } + public Action ScalingChanged { get; set; } + public Action Closed { get; set; } + public event Action LostFocus; + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs new file mode 100644 index 0000000000..5aec5408a4 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls.Platform.Surfaces; +using Avalonia.Platform; + +namespace Avalonia.LinuxFramebuffer +{ + public sealed unsafe class LinuxFramebuffer : IFramebufferPlatformSurface, IDisposable + { + private readonly Size _dpi; + private int _fd; + private fb_fix_screeninfo _fixedInfo; + private fb_var_screeninfo _varInfo; + private IntPtr _mappedLength; + private IntPtr _mappedAddress; + + public LinuxFramebuffer(string fileName = null, Size? dpi = null) + { + _dpi = dpi ?? new Size(96, 96); + fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; + _fd = NativeUnsafeMethods.open(fileName, 2, 0); + if (_fd <= 0) + throw new Exception("Error: " + Marshal.GetLastWin32Error()); + + try + { + Init(); + } + catch + { + Dispose(); + throw; + } + } + + void Init() + { + fixed (void* pnfo = &_varInfo) + { + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo)) + throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); + + SetBpp(); + + _varInfo.yoffset = 100; + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo)) + _varInfo.transp = new fb_bitfield(); + + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOPUT_VSCREENINFO, pnfo)) + throw new Exception("FBIOPUT_VSCREENINFO error: " + Marshal.GetLastWin32Error()); + + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, pnfo)) + throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); + + if (_varInfo.bits_per_pixel != 32) + throw new Exception("Unable to set 32-bit display mode"); + } + fixed(void*pnfo = &_fixedInfo) + if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_FSCREENINFO, pnfo)) + throw new Exception("FBIOGET_FSCREENINFO error: " + Marshal.GetLastWin32Error()); + + _mappedLength = new IntPtr(_fixedInfo.line_length * _varInfo.yres); + _mappedAddress = NativeUnsafeMethods.mmap(IntPtr.Zero, _mappedLength, 3, 1, _fd, IntPtr.Zero); + if (_mappedAddress == new IntPtr(-1)) + throw new Exception($"Unable to mmap {_mappedLength} bytes, error {Marshal.GetLastWin32Error()}"); + fixed (fb_fix_screeninfo* pnfo = &_fixedInfo) + { + int idlen; + for (idlen = 0; idlen < 16 && pnfo->id[idlen] != 0; idlen++) ; + Id = Encoding.ASCII.GetString(pnfo->id, idlen); + } + } + + void SetBpp() + { + _varInfo.bits_per_pixel = 32; + _varInfo.grayscale = 0; + _varInfo.red = _varInfo.blue = _varInfo.green = _varInfo.transp = new fb_bitfield + { + length = 8 + }; + _varInfo.green.offset = 8; + _varInfo.blue.offset = 16; + _varInfo.transp.offset = 24; + } + + public string Id { get; private set; } + + public Size 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); + } + } + + public ILockedFramebuffer Lock() + { + if (_fd <= 0) + throw new ObjectDisposedException("LinuxFramebuffer"); + return new LockedFramebuffer(_fd, _fixedInfo, _varInfo, _mappedAddress, _dpi); + } + + + private void ReleaseUnmanagedResources() + { + if (_mappedAddress != IntPtr.Zero) + { + NativeUnsafeMethods.munmap(_mappedAddress, _mappedLength); + _mappedAddress = IntPtr.Zero; + } + if(_fd == 0) + return; + NativeUnsafeMethods.close(_fd); + _fd = 0; + } + + public void Dispose() + { + ReleaseUnmanagedResources(); + GC.SuppressFinalize(this); + } + + ~LinuxFramebuffer() + { + ReleaseUnmanagedResources(); + } + } +} \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs new file mode 100644 index 0000000000..aaf8db9151 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Threading; +using Avalonia.Controls; +using Avalonia.Controls.Embedding; +using Avalonia.Input; +using Avalonia.Input.Platform; +using Avalonia.LinuxFramebuffer; +using Avalonia.Platform; +using Avalonia.Rendering; +using Avalonia.Threading; + +namespace Avalonia.LinuxFramebuffer +{ + class LinuxFramebufferPlatform + { + LinuxFramebuffer _fb; + public static KeyboardDevice KeyboardDevice = new KeyboardDevice(); + public static MouseDevice MouseDevice = new MouseDevice(); + private static readonly Stopwatch St = Stopwatch.StartNew(); + internal static uint Timestamp => (uint)St.ElapsedTicks; + public static FramebufferToplevelImpl TopLevel; + LinuxFramebufferPlatform(string fbdev = null) + { + _fb = new LinuxFramebuffer(fbdev); + } + + + void Initialize() + { + AvaloniaLocator.CurrentMutable + .Bind().ToTransient() + .Bind().ToConstant(KeyboardDevice) + .Bind().ToConstant(MouseDevice) + .Bind().ToSingleton() + .Bind().ToConstant(PlatformThreadingInterface.Instance) + .Bind().ToConstant(PlatformThreadingInterface.Instance); + } + + internal static TopLevel Initialize(T builder, string fbdev = null) where T : AppBuilderBase, new() + { + var platform = new LinuxFramebufferPlatform(fbdev); + builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev") + .SetupWithoutStarting(); + var tl = new EmbeddableControlRoot(TopLevel = new FramebufferToplevelImpl(platform._fb)); + tl.Prepare(); + return tl; + } + } +} + +public static class LinuxFramebufferPlatformExtensions +{ + class TokenClosable : ICloseable + { + public event EventHandler Closed; + + public TokenClosable(CancellationToken token) + { + token.Register(() => Dispatcher.UIThread.InvokeAsync(() => Closed?.Invoke(this, new EventArgs()))); + } + } + + public static void InitializeWithLinuxFramebuffer(this T builder, Action setup, + CancellationToken stop = default(CancellationToken), string fbdev = null) + where T : AppBuilderBase, new() + { + setup(LinuxFramebufferPlatform.Initialize(builder, fbdev)); + builder.BeforeStartCallback(builder); + builder.Instance.Run(new TokenClosable(stop)); + } +} + diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs new file mode 100644 index 0000000000..d8330fcb70 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/LockedFramebuffer.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.InteropServices; +using Avalonia.Platform; + +namespace Avalonia.LinuxFramebuffer +{ + unsafe class LockedFramebuffer : ILockedFramebuffer + { + private readonly int _fb; + private readonly fb_fix_screeninfo _fixedInfo; + private fb_var_screeninfo _varInfo; + private readonly IntPtr _address; + + public LockedFramebuffer(int fb, fb_fix_screeninfo fixedInfo, fb_var_screeninfo varInfo, IntPtr address, Size dpi) + { + _fb = fb; + _fixedInfo = fixedInfo; + _varInfo = varInfo; + _address = address; + Dpi = dpi; + //Use double buffering to avoid flicker + Address = Marshal.AllocHGlobal(RowBytes * Height); + } + + + void VSync() + { + NativeUnsafeMethods.ioctl(_fb, FbIoCtl.FBIO_WAITFORVSYNC, null); + } + + public void Dispose() + { + VSync(); + NativeUnsafeMethods.memcpy(_address, Address, new IntPtr(RowBytes * Height)); + + Marshal.FreeHGlobal(Address); + Address = IntPtr.Zero; + } + + public IntPtr Address { get; private set; } + public int Width => (int)_varInfo.xres; + public int Height => (int) _varInfo.yres; + public int RowBytes => (int) _fixedInfo.line_length; + public Size Dpi { get; } + public PixelFormat Format => _varInfo.blue.offset == 16 ? PixelFormat.Rgba8888 : PixelFormat.Bgra8888; + } +} \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs b/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs new file mode 100644 index 0000000000..d1cf9eefb6 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Mice.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Platform; + +namespace Avalonia.LinuxFramebuffer +{ + public unsafe class Mice + { + private readonly double _width; + private readonly double _height; + private double _x; + private double _y; + + public event Action Event; + + public Mice(double width, double height) + { + _width = width; + _height = height; + } + + public void Start() => AvaloniaLocator.Current.GetService().PostThreadPoolItem(Worker); + + 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; + + PlatformThreadingInterface.Instance.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 RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice, + LinuxFramebufferPlatform.Timestamp, + LinuxFramebufferPlatform.TopLevel.InputRoot, RawMouseEventType.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 RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice, + LinuxFramebufferPlatform.Timestamp, + LinuxFramebufferPlatform.TopLevel.InputRoot, RawMouseEventType.Move, new Point(_x, _y), + InputModifiers.None)); + } + if (ev.type == (short) EvType.EV_KEY) + { + RawMouseEventType? type = null; + if (ev.code == (ushort) EvKey.BTN_LEFT) + type = ev.value == 1 ? RawMouseEventType.LeftButtonDown : RawMouseEventType.LeftButtonUp; + if (ev.code == (ushort)EvKey.BTN_RIGHT) + type = ev.value == 1 ? RawMouseEventType.RightButtonDown : RawMouseEventType.RightButtonUp; + if (ev.code == (ushort) EvKey.BTN_MIDDLE) + type = ev.value == 1 ? RawMouseEventType.MiddleButtonDown : RawMouseEventType.MiddleButtonUp; + if (!type.HasValue) + return; + + Event?.Invoke(new RawMouseEventArgs(LinuxFramebufferPlatform.MouseDevice, + LinuxFramebufferPlatform.Timestamp, + LinuxFramebufferPlatform.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 new file mode 100644 index 0000000000..ad8def369d --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/NativeUnsafeMethods.cs @@ -0,0 +1,254 @@ +using __u32 = System.UInt32; +using __s32 = System.Int32; +using __u16 = System.UInt16; +using System; +using System.Runtime.InteropServices; +// ReSharper disable FieldCanBeMadeReadOnly.Local +// ReSharper disable ArrangeTypeMemberModifiers +// ReSharper disable BuiltInTypeReferenceStyle +// ReSharper disable InconsistentNaming + +namespace Avalonia.LinuxFramebuffer +{ + unsafe class NativeUnsafeMethods + { + [DllImport("libc", EntryPoint = "open", SetLastError = true)] + public static extern int open(string pathname, int flags, int mode); + + [DllImport("libc", EntryPoint = "close", SetLastError = true)] + public static extern int close(int fd); + + [DllImport("libc", EntryPoint = "ioctl", SetLastError = true)] + public static extern int ioctl(int fd, FbIoCtl code, void* arg); + + [DllImport("libc", EntryPoint = "mmap", SetLastError = true)] + public static extern IntPtr mmap(IntPtr addr, IntPtr length, int prot, int flags, + int fd, IntPtr offset); + [DllImport("libc", EntryPoint = "munmap", SetLastError = true)] + public static extern int munmap(IntPtr addr, IntPtr length); + + [DllImport("libc", EntryPoint = "memcpy", SetLastError = true)] + public static extern int memcpy(IntPtr dest, IntPtr src, IntPtr length); + + [DllImport("libc", EntryPoint = "select", SetLastError = true)] + public static extern int select(int nfds, void* rfds, void* wfds, void* exfds, IntPtr* timevals); + + [DllImport("libevdev.so.2", EntryPoint = "libevdev_new_from_fd", SetLastError = true)] + public static extern int libevdev_new_from_fd(int fd, out IntPtr dev); + + [DllImport("libevdev.so.2", EntryPoint = "libevdev_has_event_type", SetLastError = true)] + public static extern int libevdev_has_event_type(IntPtr dev, EvType type); + + [DllImport("libevdev.so.2", EntryPoint = "libevdev_next_event", SetLastError = true)] + public static extern int libevdev_next_event(IntPtr dev, int flags, out input_event ev); + + [DllImport("libevdev.so.2", EntryPoint = "libevdev_get_name", SetLastError = true)] + public static extern IntPtr libevdev_get_name(IntPtr dev); + [DllImport("libevdev.so.2", EntryPoint = "libevdev_get_abs_info", SetLastError = true)] + public static extern input_absinfo* libevdev_get_abs_info(IntPtr dev, int code); + } + + enum FbIoCtl : uint + { + FBIOGET_VSCREENINFO = 0x4600, + FBIOPUT_VSCREENINFO = 0x4601, + FBIOGET_FSCREENINFO = 0x4602, + FBIOGET_VBLANK = 0x80204612u, + FBIO_WAITFORVSYNC = 0x40044620, + FBIOPAN_DISPLAY = 0x4606 + } + + [Flags] + enum VBlankFlags + { + FB_VBLANK_VBLANKING = 0x001 /* currently in a vertical blank */, + FB_VBLANK_HBLANKING = 0x002 /* currently in a horizontal blank */, + FB_VBLANK_HAVE_VBLANK = 0x004 /* vertical blanks can be detected */, + FB_VBLANK_HAVE_HBLANK = 0x008 /* horizontal blanks can be detected */, + FB_VBLANK_HAVE_COUNT = 0x010 /* global retrace counter is available */, + FB_VBLANK_HAVE_VCOUNT = 0x020 /* the vcount field is valid */, + FB_VBLANK_HAVE_HCOUNT = 0x040 /* the hcount field is valid */, + FB_VBLANK_VSYNCING = 0x080 /* currently in a vsync */, + FB_VBLANK_HAVE_VSYNC = 0x100 /* verical syncs can be detected */ + } + + unsafe struct fb_vblank { + public VBlankFlags flags; /* FB_VBLANK flags */ + __u32 count; /* counter of retraces since boot */ + __u32 vcount; /* current scanline position */ + __u32 hcount; /* current scandot position */ + fixed __u32 reserved[4]; /* reserved for future compatibility */ + }; + + [StructLayout(LayoutKind.Sequential)] + unsafe struct fb_fix_screeninfo + { + public fixed byte id[16]; /* identification string eg "TT Builtin" */ + + public IntPtr smem_start; /* Start of frame buffer mem */ + + /* (physical address) */ + public __u32 smem_len; /* Length of frame buffer mem */ + + public __u32 type; /* see FB_TYPE_* */ + public __u32 type_aux; /* Interleave for interleaved Planes */ + public __u32 visual; /* see FB_VISUAL_* */ + public __u16 xpanstep; /* zero if no hardware panning */ + public __u16 ypanstep; /* zero if no hardware panning */ + public __u16 ywrapstep; /* zero if no hardware ywrap */ + public __u32 line_length; /* length of a line in bytes */ + + public IntPtr mmio_start; /* Start of Memory Mapped I/O */ + + /* (physical address) */ + public __u32 mmio_len; /* Length of Memory Mapped I/O */ + + public __u32 accel; /* Type of acceleration available */ + public fixed __u16 reserved[3]; /* Reserved for future compatibility */ + }; + + [StructLayout(LayoutKind.Sequential)] + struct fb_bitfield + { + public __u32 offset; /* beginning of bitfield */ + public __u32 length; /* length of bitfield */ + + public __u32 msb_right; /* != 0 : Most significant bit is */ + /* right */ + }; + + [StructLayout(LayoutKind.Sequential)] + unsafe struct fb_var_screeninfo + { + public __u32 xres; /* visible resolution */ + public __u32 yres; + public __u32 xres_virtual; /* virtual resolution */ + public __u32 yres_virtual; + public __u32 xoffset; /* offset from virtual to visible */ + public __u32 yoffset; /* resolution */ + + public __u32 bits_per_pixel; /* guess what */ + public __u32 grayscale; /* != 0 Graylevels instead of colors */ + + public fb_bitfield red; /* bitfield in fb mem if true color, */ + public fb_bitfield green; /* else only length is significant */ + public fb_bitfield blue; + public fb_bitfield transp; /* transparency */ + + public __u32 nonstd; /* != 0 Non standard pixel format */ + + public __u32 activate; /* see FB_ACTIVATE_* */ + + public __u32 height; /* height of picture in mm */ + public __u32 width; /* width of picture in mm */ + + public __u32 accel_flags; /* acceleration flags (hints) */ + + /* Timing: All values in pixclocks, except pixclock (of course) */ + public __u32 pixclock; /* pixel clock in ps (pico seconds) */ + + public __u32 left_margin; /* time from sync to picture */ + public __u32 right_margin; /* time from picture to sync */ + public __u32 upper_margin; /* time from sync to picture */ + public __u32 lower_margin; + public __u32 hsync_len; /* length of horizontal sync */ + public __u32 vsync_len; /* length of vertical sync */ + public __u32 sync; /* see FB_SYNC_* */ + public __u32 vmode; /* see FB_VMODE_* */ + public fixed __u32 reserved[6]; /* Reserved for future compatibility */ + }; + + + enum EvType + { + EV_SYN = 0x00, + EV_KEY = 0x01, + EV_REL = 0x02, + EV_ABS = 0x03, + EV_MSC = 0x04, + EV_SW = 0x05, + EV_LED = 0x11, + EV_SND = 0x12, + EV_REP = 0x14, + EV_FF = 0x15, + EV_PWR = 0x16, + EV_FF_STATUS = 0x17, + } + + [StructLayout(LayoutKind.Sequential)] + struct input_event + { + private IntPtr crap1, crap2; + public ushort type, code; + public int value; + } + + [StructLayout(LayoutKind.Sequential)] + unsafe struct fd_set + { + public int count; + public fixed int fds [256]; + } + + enum AxisEventCode + { + REL_X = 0x00, + REL_Y = 0x01, + REL_Z = 0x02, + REL_RX = 0x03, + REL_RY = 0x04, + REL_RZ = 0x05, + REL_HWHEEL = 0x06, + REL_DIAL = 0x07, + REL_WHEEL = 0x08, + REL_MISC = 0x09, + REL_MAX = 0x0f + } + + enum AbsAxis + { + ABS_X = 0x00, + ABS_Y = 0x01, + ABS_Z = 0x02, + ABS_RX = 0x03, + ABS_RY = 0x04, + ABS_RZ = 0x05, + ABS_THROTTLE = 0x06, + ABS_RUDDER = 0x07, + ABS_WHEEL = 0x08, + ABS_GAS = 0x09, + ABS_BRAKE = 0x0a, + ABS_HAT0X = 0x10, + ABS_HAT0Y = 0x11, + ABS_HAT1X = 0x12, + ABS_HAT1Y = 0x13, + ABS_HAT2X = 0x14, + ABS_HAT2Y = 0x15, + ABS_HAT3X = 0x16, + ABS_HAT3Y = 0x17, + ABS_PRESSURE = 0x18, + ABS_DISTANCE = 0x19, + ABS_TILT_X = 0x1a, + ABS_TILT_Y = 0x1b, + ABS_TOOL_WIDTH = 0x1c + } + + enum EvKey + { + BTN_LEFT = 0x110, + BTN_RIGHT = 0x111, + BTN_MIDDLE = 0x112 + } + + [StructLayout(LayoutKind.Sequential)] + struct input_absinfo + { + public __s32 value; + public __s32 minimum; + public __s32 maximum; + public __s32 fuzz; + public __s32 flat; + public __s32 resolution; + + } +} \ No newline at end of file diff --git a/src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs b/src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs new file mode 100644 index 0000000000..9231649754 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/PlatformThreadingInterface.cs @@ -0,0 +1,112 @@ +using System; +using System.Collections.Generic; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Platform; +using Avalonia.Rendering; + +namespace Avalonia.LinuxFramebuffer +{ + class PlatformThreadingInterface : IPlatformThreadingInterface, IRenderLoop + { + public static PlatformThreadingInterface Instance { get; } = new PlatformThreadingInterface(); + + public PlatformThreadingInterface() + { + TlsCurrentThreadIsLoopThread = true; + StartTimer(new TimeSpan(0, 0, 0, 0, 66), () => Tick?.Invoke(this, new EventArgs())); + } + + 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(); + else + { + while (true) + { + Action item; + lock(_actions) + if (_actions.Count == 0) + break; + else + item = _actions.Dequeue(); + item(); + } + } + } + } + + public void Send(Action cb) + { + lock (_actions) + { + _actions.Enqueue(cb); + _queued.Set(); + } + } + + class WatTimer : IDisposable + { + private readonly IDisposable _timer; + private GCHandle _handle; + + public WatTimer(IDisposable timer) + { + _timer = timer; + _handle = GCHandle.Alloc(_timer); + } + public void Dispose() + { + _handle.Free(); + _timer.Dispose(); + } + } + + public IDisposable StartTimer(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)); + + + } + + public void Signal() + { + _signaled.Set(); + } + + [ThreadStatic] private static bool TlsCurrentThreadIsLoopThread; + + public bool CurrentThreadIsLoopThread => TlsCurrentThreadIsLoopThread; + public event Action Signaled; + public event EventHandler Tick; + + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs new file mode 100644 index 0000000000..67d62e3410 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Stubs.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Input; +using Avalonia.Platform; + +namespace Avalonia.LinuxFramebuffer +{ + internal class CursorFactoryStub : IStandardCursorFactory + { + public IPlatformHandle GetCursor(StandardCursorType cursorType) + { + return new PlatformHandle(IntPtr.Zero, null); + } + } + internal class PlatformSettings : IPlatformSettings + { + public Size DoubleClickSize { get; } = new Size(4, 4); + public TimeSpan DoubleClickTime { get; } = new TimeSpan(0, 0, 0, 0, 500); + } +}