From 6f74b568b23ad604977ccb2c0b662a6123779c90 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Wed, 24 Jul 2019 01:01:22 -0800 Subject: [PATCH] Implemented libdrm backend with OpenGL acceleration support --- samples/ControlCatalog.NetCore/Program.cs | 21 +- src/Avalonia.OpenGL/EglContext.cs | 8 +- src/Avalonia.OpenGL/EglDisplay.cs | 94 +++--- src/Avalonia.OpenGL/EglInterface.cs | 5 + src/Avalonia.OpenGL/GlInterface.cs | 2 +- .../FramebufferToplevelImpl.cs | 18 +- .../LinuxFramebufferPlatform.cs | 29 +- .../Avalonia.LinuxFramebuffer/Output/Drm.cs | 292 ++++++++++++++++++ .../Output/DrmBindings.cs | 158 ++++++++++ .../Output/DrmOutput.cs | 250 +++++++++++++++ .../FbdevOutput.cs} | 11 +- .../Output/IOutputBackend.cs | 7 + 12 files changed, 828 insertions(+), 67 deletions(-) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs rename src/Linux/Avalonia.LinuxFramebuffer/{LinuxFramebuffer.cs => Output/FbdevOutput.cs} (92%) create mode 100644 src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs index 40321496c0..de9ca02ed1 100644 --- a/samples/ControlCatalog.NetCore/Program.cs +++ b/samples/ControlCatalog.NetCore/Program.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading; using Avalonia; using Avalonia.Controls; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Skia; using Avalonia.ReactiveUI; @@ -29,8 +30,13 @@ namespace ControlCatalog.NetCore var builder = BuildAvaloniaApp(); if (args.Contains("--fbdev")) { - System.Threading.ThreadPool.QueueUserWorkItem(_ => ConsoleSilencer()); - return builder.StartLinuxFramebuffer(args); + SilenceConsole(); + return builder.StartLinuxFbDev(args); + } + else if (args.Contains("--drm")) + { + SilenceConsole(); + return builder.StartLinuxDrm(args); } else return builder.StartWithClassicDesktopLifetime(args); @@ -51,11 +57,14 @@ namespace ControlCatalog.NetCore .UseSkia() .UseReactiveUI(); - static void ConsoleSilencer() + static void SilenceConsole() { - Console.CursorVisible = false; - while (true) - Console.ReadKey(true); + new Thread(() => + { + Console.CursorVisible = false; + while (true) + Console.ReadKey(true); + }) {IsBackground = true}.Start(); } } } diff --git a/src/Avalonia.OpenGL/EglContext.cs b/src/Avalonia.OpenGL/EglContext.cs index 17caf84179..a39000f198 100644 --- a/src/Avalonia.OpenGL/EglContext.cs +++ b/src/Avalonia.OpenGL/EglContext.cs @@ -10,7 +10,7 @@ namespace Avalonia.OpenGL private readonly EglInterface _egl; private readonly object _lock = new object(); - public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, IntPtr offscreenSurface) + public EglContext(EglDisplay display, EglInterface egl, IntPtr ctx, EglSurface offscreenSurface) { _disp = display; _egl = egl; @@ -19,7 +19,7 @@ namespace Avalonia.OpenGL } public IntPtr Context { get; } - public IntPtr OffscreenSurface { get; } + public EglSurface OffscreenSurface { get; } public IGlDisplay Display => _disp; public IDisposable Lock() @@ -36,8 +36,8 @@ namespace Avalonia.OpenGL public void MakeCurrent(EglSurface surface) { - var surf = surface?.DangerousGetHandle() ?? OffscreenSurface; - if (!_egl.MakeCurrent(_disp.Handle, surf, surf, Context)) + var surf = surface ?? OffscreenSurface; + if (!_egl.MakeCurrent(_disp.Handle, surf.DangerousGetHandle(), surf.DangerousGetHandle(), Context)) throw OpenGlException.GetFormattedException("eglMakeCurrent", _egl); } } diff --git a/src/Avalonia.OpenGL/EglDisplay.cs b/src/Avalonia.OpenGL/EglDisplay.cs index b2b5a1a646..66418c0e15 100644 --- a/src/Avalonia.OpenGL/EglDisplay.cs +++ b/src/Avalonia.OpenGL/EglDisplay.cs @@ -12,49 +12,62 @@ namespace Avalonia.OpenGL private readonly IntPtr _display; private readonly IntPtr _config; private readonly int[] _contextAttributes; + private readonly int _surfaceType; public IntPtr Handle => _display; private AngleOptions.PlatformApi? _angleApi; - public EglDisplay(EglInterface egl) + + public EglDisplay(EglInterface egl) : this(egl, -1, IntPtr.Zero, null) + { + + } + public EglDisplay(EglInterface egl, int platformType, IntPtr platformDisplay, int[] attrs) { - _egl = egl; + _egl = egl; - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + if (platformType == -1 && platformDisplay == IntPtr.Zero) { - if (_egl.GetPlatformDisplayEXT == null) - throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); - - var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis - ?? new List {AngleOptions.PlatformApi.DirectX9}; - - foreach (var platformApi in allowedApis) + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - int dapi; - if (platformApi == AngleOptions.PlatformApi.DirectX9) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; - else if (platformApi == AngleOptions.PlatformApi.DirectX11) - dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; - else - continue; - - _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, new[] - { - EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE - }); - if (_display != IntPtr.Zero) + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl.dll"); + + var allowedApis = AvaloniaLocator.Current.GetService()?.AllowedPlatformApis + ?? new List {AngleOptions.PlatformApi.DirectX9}; + + foreach (var platformApi in allowedApis) { - _angleApi = platformApi; - break; + int dapi; + if (platformApi == AngleOptions.PlatformApi.DirectX9) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D9_ANGLE; + else if (platformApi == AngleOptions.PlatformApi.DirectX11) + dapi = EGL_PLATFORM_ANGLE_TYPE_D3D11_ANGLE; + else + continue; + + _display = _egl.GetPlatformDisplayEXT(EGL_PLATFORM_ANGLE_ANGLE, IntPtr.Zero, + new[] {EGL_PLATFORM_ANGLE_TYPE_ANGLE, dapi, EGL_NONE}); + if (_display != IntPtr.Zero) + { + _angleApi = platformApi; + break; + } } + + if (_display == IntPtr.Zero) + throw new OpenGlException("Unable to create ANGLE display"); } if (_display == IntPtr.Zero) - throw new OpenGlException("Unable to create ANGLE display"); + _display = _egl.GetDisplay(IntPtr.Zero); + } + else + { + if (_egl.GetPlatformDisplayEXT == null) + throw new OpenGlException("eglGetPlatformDisplayEXT is not supported by libegl"); + _display = _egl.GetPlatformDisplayEXT(platformType, platformDisplay, attrs); } - if (_display == IntPtr.Zero) - _display = _egl.GetDisplay(IntPtr.Zero); - if (_display == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglGetDisplay", _egl); @@ -85,16 +98,14 @@ namespace Avalonia.OpenGL { if (!_egl.BindApi(cfg.Api)) continue; - + foreach(var surfaceType in new[]{EGL_PBUFFER_BIT|EGL_WINDOW_BIT, EGL_WINDOW_BIT}) foreach(var stencilSize in new[]{8, 1, 0}) foreach (var depthSize in new []{8, 1, 0}) { var attribs = new[] { - EGL_SURFACE_TYPE, EGL_PBUFFER_BIT, - + EGL_SURFACE_TYPE, surfaceType, EGL_RENDERABLE_TYPE, cfg.RenderableTypeBit, - EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8, EGL_BLUE_SIZE, 8, @@ -108,6 +119,7 @@ namespace Avalonia.OpenGL if (numConfigs == 0) continue; _contextAttributes = cfg.Attributes; + _surfaceType = surfaceType; Type = cfg.Type; } } @@ -126,8 +138,10 @@ namespace Avalonia.OpenGL public GlDisplayType Type { get; } public GlInterface GlInterface { get; } public EglInterface EglInterface => _egl; - public IGlContext CreateContext(IGlContext share) + public EglContext CreateContext(IGlContext share) { + if((_surfaceType|EGL_PBUFFER_BIT) == 0) + throw new InvalidOperationException("Platform doesn't support PBUFFER surfaces"); var shareCtx = (EglContext)share; var ctx = _egl.CreateContext(_display, _config, shareCtx?.Context ?? IntPtr.Zero, _contextAttributes); if (ctx == IntPtr.Zero) @@ -140,7 +154,17 @@ namespace Avalonia.OpenGL }); if (surf == IntPtr.Zero) throw OpenGlException.GetFormattedException("eglCreatePBufferSurface", _egl); - var rv = new EglContext(this, _egl, ctx, surf); + var rv = new EglContext(this, _egl, ctx, new EglSurface(this, _egl, surf)); + rv.MakeCurrent(null); + return rv; + } + + public EglContext CreateContext(EglContext share, EglSurface offscreenSurface) + { + var ctx = _egl.CreateContext(_display, _config, share?.Context ?? IntPtr.Zero, _contextAttributes); + if (ctx == IntPtr.Zero) + throw OpenGlException.GetFormattedException("eglCreateContext", _egl); + var rv = new EglContext(this, _egl, ctx, offscreenSurface); rv.MakeCurrent(null); return rv; } diff --git a/src/Avalonia.OpenGL/EglInterface.cs b/src/Avalonia.OpenGL/EglInterface.cs index 0a99778ddf..47088972a4 100644 --- a/src/Avalonia.OpenGL/EglInterface.cs +++ b/src/Avalonia.OpenGL/EglInterface.cs @@ -10,6 +10,11 @@ namespace Avalonia.OpenGL public EglInterface() : base(Load()) { + } + + public EglInterface(Func getProcAddress) : base(getProcAddress) + { + } public EglInterface(string library) : base(Load(library)) diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index f556949cfa..30f7d67152 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -39,7 +39,7 @@ namespace Avalonia.OpenGL [GlEntryPoint("glClearStencil")] public GlClearStencil ClearStencil { get; } - public delegate void GlClearColor(int r, int g, int b, int a); + public delegate void GlClearColor(float r, float g, float b, float a); [GlEntryPoint("glClearColor")] public GlClearColor ClearColor { get; } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 3376fa8a32..5e2ba51caf 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.LinuxFramebuffer.Input; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -11,14 +12,14 @@ namespace Avalonia.LinuxFramebuffer { class FramebufferToplevelImpl : IEmbeddableWindowImpl, IScreenInfoProvider { - private readonly LinuxFramebuffer _fb; + private readonly IOutputBackend _outputBackend; private readonly IInputBackend _inputBackend; private bool _renderQueued; public IInputRoot InputRoot { get; private set; } - public FramebufferToplevelImpl(LinuxFramebuffer fb, IInputBackend inputBackend) + public FramebufferToplevelImpl(IOutputBackend outputBackend, IInputBackend inputBackend) { - _fb = fb; + _outputBackend = outputBackend; _inputBackend = inputBackend; Invalidate(default(Rect)); _inputBackend.Initialize(this, e => Input?.Invoke(e)); @@ -26,7 +27,10 @@ namespace Avalonia.LinuxFramebuffer public IRenderer CreateRenderer(IRenderRoot root) { - return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()); + return new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) + { + + }; } public void Dispose() @@ -53,10 +57,10 @@ namespace Avalonia.LinuxFramebuffer { } - public Size ClientSize => _fb.PixelSize; + public Size ClientSize => ScaledSize; public IMouseDevice MouseDevice => new MouseDevice(); public double Scaling => 1; - public IEnumerable Surfaces => new object[] {_fb}; + public IEnumerable Surfaces => new object[] {_outputBackend}; public Action Input { get; set; } public Action Paint { get; set; } public Action Resized { get; set; } @@ -68,6 +72,6 @@ namespace Avalonia.LinuxFramebuffer remove {} } - public Size ScaledSize => _fb.PixelSize / Scaling; + public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling); } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs index 85e3f97b74..2cc1f65202 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebufferPlatform.cs @@ -9,6 +9,8 @@ using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.LinuxFramebuffer; using Avalonia.LinuxFramebuffer.Input.LibInput; +using Avalonia.LinuxFramebuffer.Output; +using Avalonia.OpenGL; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Threading; @@ -17,19 +19,21 @@ namespace Avalonia.LinuxFramebuffer { class LinuxFramebufferPlatform { - LinuxFramebuffer _fb; + IOutputBackend _fb; private static readonly Stopwatch St = Stopwatch.StartNew(); internal static uint Timestamp => (uint)St.ElapsedTicks; public static InternalPlatformThreadingInterface Threading; - LinuxFramebufferPlatform(string fbdev = null) + LinuxFramebufferPlatform(IOutputBackend backend) { - _fb = new LinuxFramebuffer(fbdev); + _fb = backend; } void Initialize() { Threading = new InternalPlatformThreadingInterface(); + if (_fb is IWindowingPlatformGlFeature glFeature) + AvaloniaLocator.CurrentMutable.Bind().ToConstant(glFeature); AvaloniaLocator.CurrentMutable .Bind().ToConstant(Threading) .Bind().ToConstant(new DefaultRenderTimer(60)) @@ -42,9 +46,10 @@ namespace Avalonia.LinuxFramebuffer } - internal static LinuxFramebufferLifetime Initialize(T builder, string fbdev = null) where T : AppBuilderBase, new() + + internal static LinuxFramebufferLifetime Initialize(T builder, IOutputBackend outputBackend) where T : AppBuilderBase, new() { - var platform = new LinuxFramebufferPlatform(fbdev); + var platform = new LinuxFramebufferPlatform(outputBackend); builder.UseSkia().UseWindowingSubsystem(platform.Initialize, "fbdev"); return new LinuxFramebufferLifetime(platform._fb); } @@ -52,12 +57,12 @@ namespace Avalonia.LinuxFramebuffer class LinuxFramebufferLifetime : IControlledApplicationLifetime, ISingleViewApplicationLifetime { - private readonly LinuxFramebuffer _fb; + private readonly IOutputBackend _fb; private TopLevel _topLevel; private readonly CancellationTokenSource _cts = new CancellationTokenSource(); public CancellationToken Token => _cts.Token; - public LinuxFramebufferLifetime(LinuxFramebuffer fb) + public LinuxFramebufferLifetime(IOutputBackend fb) { _fb = fb; } @@ -102,10 +107,16 @@ namespace Avalonia.LinuxFramebuffer public static class LinuxFramebufferPlatformExtensions { - public static int StartLinuxFramebuffer(this T builder, string[] args, string fbdev = null) + public static int StartLinuxFbDev(this T builder, string[] args, string fbdev = null) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new FbdevOutput(fbdev)); + + public static int StartLinuxDrm(this T builder, string[] args, string card = null) + where T : AppBuilderBase, new() => StartLinuxDirect(builder, args, new DrmOutput(card)); + + public static int StartLinuxDirect(this T builder, string[] args, IOutputBackend backend) where T : AppBuilderBase, new() { - var lifetime = LinuxFramebufferPlatform.Initialize(builder, fbdev); + var lifetime = LinuxFramebufferPlatform.Initialize(builder, backend); builder.Instance.ApplicationLifetime = lifetime; builder.SetupWithoutStarting(); lifetime.Start(args); diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs new file mode 100644 index 0000000000..e266c5ee54 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/Drm.cs @@ -0,0 +1,292 @@ +using System; +using System.Runtime.InteropServices; +// ReSharper disable FieldCanBeMadeReadOnly.Global +// ReSharper disable MemberCanBePrivate.Global +// ReSharper disable FieldCanBeMadeReadOnly.Local + +namespace Avalonia.LinuxFramebuffer.Output +{ + public enum DrmModeConnection + { + DRM_MODE_CONNECTED = 1, + DRM_MODE_DISCONNECTED = 2, + DRM_MODE_UNKNOWNCONNECTION = 3 + } + + public enum DrmModeSubPixel{ + DRM_MODE_SUBPIXEL_UNKNOWN = 1, + DRM_MODE_SUBPIXEL_HORIZONTAL_RGB = 2, + DRM_MODE_SUBPIXEL_HORIZONTAL_BGR = 3, + DRM_MODE_SUBPIXEL_VERTICAL_RGB = 4, + DRM_MODE_SUBPIXEL_VERTICAL_BGR = 5, + DRM_MODE_SUBPIXEL_NONE = 6 + } + + static unsafe class LibDrm + { + private const string libdrm = "libdrm.so.2"; + private const string libgbm = "libgbm.so.1"; + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void DrmEventVBlankHandlerDelegate(int fd, + uint sequence, + uint tv_sec, + uint tv_usec, + void* user_data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void DrmEventPageFlipHandlerDelegate(int fd, + uint sequence, + uint tv_sec, + uint tv_usec, + void* user_data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate IntPtr DrmEventPageFlipHandler2Delegate(int fd, + uint sequence, + uint tv_sec, + uint tv_usec, + uint crtc_id, + void* user_data); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public unsafe delegate void DrmEventSequenceHandlerDelegate(int fd, + ulong sequence, + ulong ns, + ulong user_data); + + [StructLayout(LayoutKind.Sequential)] + public struct DrmEventContext + { + public int version; //4 + public IntPtr vblank_handler; + public IntPtr page_flip_handler; + public IntPtr page_flip_handler2; + public IntPtr sequence_handler; + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeRes { + + public int count_fbs; + public uint *fbs; + + public int count_crtcs; + public uint *crtcs; + + public int count_connectors; + public uint *connectors; + + public int count_encoders; + public uint *encoders; + + uint min_width, max_width; + uint min_height, max_height; + } + + [Flags] + public enum DrmModeType + { + DRM_MODE_TYPE_BUILTIN = (1 << 0), + DRM_MODE_TYPE_CLOCK_C = ((1 << 1) | DRM_MODE_TYPE_BUILTIN), + DRM_MODE_TYPE_CRTC_C = ((1 << 2) | DRM_MODE_TYPE_BUILTIN), + DRM_MODE_TYPE_PREFERRED = (1 << 3), + DRM_MODE_TYPE_DEFAULT = (1 << 4), + DRM_MODE_TYPE_USERDEF = (1 << 5), + DRM_MODE_TYPE_DRIVER = (1 << 6) + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeModeInfo + { + public uint clock; + public ushort hdisplay, hsync_start, hsync_end, htotal, hskew; + public ushort vdisplay, vsync_start, vsync_end, vtotal, vscan; + + public uint vrefresh; + + public uint flags; + public DrmModeType type; + public fixed byte name[32]; + public PixelSize Resolution => new PixelSize(hdisplay, vdisplay); + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeConnector { + public uint connector_id; + public uint encoder_id; /**< Encoder currently connected to */ + public uint connector_type; + public uint connector_type_id; + public DrmModeConnection connection; + public uint mmWidth, mmHeight; /**< HxW in millimeters */ + public DrmModeSubPixel subpixel; + + public int count_modes; + public drmModeModeInfo* modes; + + public int count_props; + public uint *props; /**< List of property ids */ + public ulong *prop_values; /**< List of property values */ + + public int count_encoders; + public uint *encoders; /**< List of encoder ids */ + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeEncoder { + public uint encoder_id; + public uint encoder_type; + public uint crtc_id; + public uint possible_crtcs; + public uint possible_clones; + } + + [StructLayout(LayoutKind.Sequential)] + public struct drmModeCrtc { + public uint crtc_id; + public uint buffer_id; /**< FB id to connect to 0 = disconnect */ + + public uint x, y; /**< Position on the framebuffer */ + public uint width, height; + public int mode_valid; + public drmModeModeInfo mode; + + public int gamma_size; /**< Number of gamma stops */ + + } + + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeRes* drmModeGetResources(int fd); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeResources(drmModeRes* res); + + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeConnector* drmModeGetConnector(int fd, uint connector); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeConnector(drmModeConnector* res); + + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeEncoder* drmModeGetEncoder(int fd, uint id); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeEncoder(drmModeEncoder* enc); + [DllImport(libdrm, SetLastError = true)] + public static extern drmModeCrtc* drmModeGetCrtc(int fd, uint id); + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeFreeCrtc(drmModeCrtc* enc); + + [DllImport(libdrm, SetLastError = true)] + public static extern int drmModeAddFB(int fd, uint width, uint height, byte depth, + byte bpp, uint pitch, uint bo_handle, + out uint buf_id); + [DllImport(libdrm, SetLastError = true)] + public static extern int drmModeSetCrtc(int fd, uint crtcId, uint bufferId, + uint x, uint y, uint *connectors, int count, + drmModeModeInfo* mode); + + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModeRmFB(int fd, int id); + + [Flags] + public enum DrmModePageFlip + { + Event = 1, + Async = 2, + Absolute = 4, + Relative = 8, + } + + [DllImport(libdrm, SetLastError = true)] + public static extern void drmModePageFlip(int fd, uint crtc_id, uint fb_id, + DrmModePageFlip flags, void *user_data); + + + [DllImport(libdrm, SetLastError = true)] + public static extern void drmHandleEvent(int fd, DrmEventContext* context); + + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_create_device(int fd); + + + [Flags] + public enum GbmBoFlags { + /** + * Buffer is going to be presented to the screen using an API such as KMS + */ + GBM_BO_USE_SCANOUT = (1 << 0), + /** + * Buffer is going to be used as cursor + */ + GBM_BO_USE_CURSOR = (1 << 1), + /** + * Deprecated + */ + GBM_BO_USE_CURSOR_64X64 = GBM_BO_USE_CURSOR, + /** + * Buffer is to be used for rendering - for example it is going to be used + * as the storage for a color buffer + */ + GBM_BO_USE_RENDERING = (1 << 2), + /** + * Buffer can be used for gbm_bo_write. This is guaranteed to work + * with GBM_BO_USE_CURSOR, but may not work for other combinations. + */ + GBM_BO_USE_WRITE = (1 << 3), + /** + * Buffer is linear, i.e. not tiled. + */ + GBM_BO_USE_LINEAR = (1 << 4), + }; + + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_surface_create(IntPtr device, int width, int height, uint format, GbmBoFlags flags); + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_surface_lock_front_buffer(IntPtr surface); + [DllImport(libgbm, SetLastError = true)] + public static extern int gbm_surface_release_buffer(IntPtr surface, IntPtr bo); + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_bo_get_user_data(IntPtr surface); + + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] + public delegate void GbmBoUserDataDestroyCallbackDelegate(IntPtr bo, IntPtr data); + + [DllImport(libgbm, SetLastError = true)] + public static extern IntPtr gbm_bo_set_user_data(IntPtr bo, IntPtr userData, + GbmBoUserDataDestroyCallbackDelegate onFree); + + [DllImport(libgbm, SetLastError = true)] + public static extern uint gbm_bo_get_width(IntPtr bo); + + [DllImport(libgbm, SetLastError = true)] + public static extern uint gbm_bo_get_height(IntPtr bo); + + [DllImport(libgbm, SetLastError = true)] + public static extern uint gbm_bo_get_stride(IntPtr bo); + + + [StructLayout(LayoutKind.Explicit)] + public struct GbmBoHandle + { + [FieldOffset(0)] + public void *ptr; + [FieldOffset(0)] + public int s32; + [FieldOffset(0)] + public uint u32; + [FieldOffset(0)] + public long s64; + [FieldOffset(0)] + public ulong u64; + } + + [DllImport(libgbm, SetLastError = true)] + public static extern ulong gbm_bo_get_handle(IntPtr bo); + + public static class GbmColorFormats + { + public static uint FourCC(char a, char b, char c, char d) => + (uint)a | ((uint)b) << 8 | ((uint)c) << 16 | ((uint)d) << 24; + + public static uint GBM_FORMAT_XRGB8888 { get; } = FourCC('X', 'R', '2', '4'); + } + } + +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs new file mode 100644 index 0000000000..b5ebc4bcb7 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmBindings.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.InteropServices; +using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; +using static Avalonia.LinuxFramebuffer.Output.LibDrm; + +namespace Avalonia.LinuxFramebuffer.Output +{ + public unsafe class DrmConnector + { + private static string[] KnownConnectorTypes = + { + "None", "VGA", "DVI-I", "DVI-D", "DVI-A", "Composite", "S-Video", "LVDS", "Component", "DIN", + "DisplayPort", "HDMI-A", "HDMI-B", "TV", "eDP", "Virtual", "DSI" + }; + + public DrmModeConnection Connection { get; } + public uint Id { get; } + public string Name { get; } + public Size SizeMm { get; } + public DrmModeSubPixel SubPixel { get; } + internal uint EncoderId { get; } + internal List EncoderIds { get; } = new List(); + public List Modes { get; } = new List(); + internal DrmConnector(drmModeConnector* conn) + { + Connection = conn->connection; + Id = conn->connector_id; + SizeMm = new Size(conn->mmWidth, conn->mmHeight); + SubPixel = conn->subpixel; + for (var c = 0; c < conn->count_encoders;c++) + EncoderIds.Add(conn->encoders[c]); + EncoderId = conn->encoder_id; + for(var c=0; ccount_modes; c++) + Modes.Add(new DrmModeInfo(ref conn->modes[c])); + + if (conn->connector_type > KnownConnectorTypes.Length - 1) + Name = $"Unknown({conn->connector_type})-{conn->connector_type_id}"; + else + Name = KnownConnectorTypes[conn->connector_type] + "-" + conn->connector_type_id; + } + } + + public unsafe class DrmModeInfo + { + internal drmModeModeInfo Mode; + + internal DrmModeInfo(ref drmModeModeInfo info) + { + Mode = info; + fixed (void* pName = info.name) + Name = Marshal.PtrToStringAnsi(new IntPtr(pName)); + } + + public PixelSize Resolution => new PixelSize(Mode.hdisplay, Mode.vdisplay); + public bool IsPreferred => Mode.type.HasFlag(DrmModeType.DRM_MODE_TYPE_PREFERRED); + + public string Name { get; } + } + + unsafe class DrmEncoder + { + public drmModeEncoder Encoder { get; } + public List PossibleCrtcs { get; } = new List(); + + public DrmEncoder(drmModeEncoder encoder, drmModeCrtc[] crtcs) + { + Encoder = encoder; + for (var c = 0; c < crtcs.Length; c++) + { + var bit = 1 << c; + if ((encoder.possible_crtcs & bit) != 0) + PossibleCrtcs.Add(crtcs[c]); + } + } + } + + + + public unsafe class DrmResources + { + public List Connectors { get; }= new List(); + internal Dictionary Encoders { get; } = new Dictionary(); + public DrmResources(int fd) + { + var res = drmModeGetResources(fd); + if (res == null) + throw new Win32Exception("drmModeGetResources failed"); + + var crtcs = new drmModeCrtc[res->count_crtcs]; + for (var c = 0; c < res->count_crtcs; c++) + { + var crtc = drmModeGetCrtc(fd, res->crtcs[c]); + crtcs[c] = *crtc; + drmModeFreeCrtc(crtc); + } + + for (var c = 0; c < res->count_encoders; c++) + { + var enc = drmModeGetEncoder(fd, res->encoders[c]); + Encoders[res->encoders[c]] = new DrmEncoder(*enc, crtcs); + drmModeFreeEncoder(enc); + } + + for (var c = 0; c < res->count_connectors; c++) + { + var conn = drmModeGetConnector(fd, res->connectors[c]); + Connectors.Add(new DrmConnector(conn)); + drmModeFreeConnector(conn); + } + + + } + + public void Dump() + { + void Print(int off, string s) + { + for (var c = 0; c < off; c++) + Console.Write(" "); + Console.WriteLine(s); + } + Print(0, "Connectors"); + foreach (var conn in Connectors) + { + Print(1, $"{conn.Name}:"); + Print(2, $"Id: {conn.Id}"); + Print(2, $"Size: {conn.SizeMm} mm"); + Print(2, $"Encoder id: {conn.EncoderId}"); + Print(2, "Modes"); + foreach (var m in conn.Modes) + Print(3, $"{m.Name} {(m.IsPreferred ? "PREFERRED" : "")}"); + + + } + } + } + + public unsafe class DrmCard : IDisposable + { + public int Fd { get; private set; } + public DrmCard(string path = null) + { + path = path ?? "/dev/dri/card0"; + Fd = open(path, 2, 0); + if (Fd == -1) + throw new Win32Exception("Couldn't open " + path); + } + + public DrmResources GetResources() => new DrmResources(Fd); + public void Dispose() + { + close(Fd); + Fd = -1; + } + } +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs new file mode 100644 index 0000000000..6a76977352 --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/DrmOutput.cs @@ -0,0 +1,250 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using Avalonia.OpenGL; +using Avalonia.Platform.Interop; +using static Avalonia.LinuxFramebuffer.NativeUnsafeMethods; +using static Avalonia.LinuxFramebuffer.Output.LibDrm; +namespace Avalonia.LinuxFramebuffer.Output +{ + public unsafe class DrmOutput : IOutputBackend, IGlPlatformSurface, IWindowingPlatformGlFeature + { + private DrmCard _card; + private readonly EglGlPlatformSurface _eglPlatformSurface; + public PixelSize PixelSize => _mode.Resolution; + + public DrmOutput(string path = null) + { + var card = new DrmCard(path); + + var resources = card.GetResources(); + + + var connector = + resources.Connectors.FirstOrDefault(x => x.Connection == DrmModeConnection.DRM_MODE_CONNECTED); + if(connector == null) + throw new InvalidOperationException("Unable to find connected DRM connector"); + + var mode = connector.Modes.OrderByDescending(x => x.IsPreferred) + .ThenByDescending(x => x.Resolution.Width * x.Resolution.Height) + //.OrderByDescending(x => x.Resolution.Width * x.Resolution.Height) + .FirstOrDefault(); + if(mode == null) + throw new InvalidOperationException("Unable to find a usable DRM mode"); + Init(card, resources, connector, mode); + } + + public DrmOutput(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo) + { + Init(card, resources, connector, modeInfo); + } + + [DllImport("libEGL.so.1")] + static extern IntPtr eglGetProcAddress(Utf8Buffer proc); + + private GbmBoUserDataDestroyCallbackDelegate FbDestroyDelegate; + private drmModeModeInfo _mode; + private EglDisplay _eglDisplay; + private EglSurface _eglSurface; + private EglContext _immediateContext; + private EglContext _deferredContext; + private IntPtr _currentBo; + private IntPtr _gbmTargetSurface; + private uint _crtcId; + + void FbDestroyCallback(IntPtr bo, IntPtr userData) + { + drmModeRmFB(_card.Fd, userData.ToInt32()); + } + + uint GetFbIdForBo(IntPtr bo) + { + if (bo == IntPtr.Zero) + throw new ArgumentException("bo is 0"); + var data = gbm_bo_get_user_data(bo); + if (data != IntPtr.Zero) + return (uint)data.ToInt32(); + + var w = gbm_bo_get_width(bo); + var h = gbm_bo_get_height(bo); + var stride = gbm_bo_get_stride(bo); + var handle = gbm_bo_get_handle(bo); + + var ret = drmModeAddFB(_card.Fd, w, h, 24, 32, stride, (uint)handle, out var fbHandle); + if (ret != 0) + throw new Win32Exception(ret, "drmModeAddFb failed"); + + gbm_bo_set_user_data(bo, new IntPtr((int)fbHandle), FbDestroyDelegate); + + + return fbHandle; + } + + + void Init(DrmCard card, DrmResources resources, DrmConnector connector, DrmModeInfo modeInfo) + { + FbDestroyDelegate = FbDestroyCallback; + _card = card; + uint GetCrtc() + { + if (resources.Encoders.TryGetValue(connector.EncoderId, out var encoder)) + { + // Not sure why that should work + return encoder.Encoder.crtc_id; + } + else + { + foreach (var encId in connector.EncoderIds) + { + if (resources.Encoders.TryGetValue(encId, out encoder) + && encoder.PossibleCrtcs.Count>0) + return encoder.PossibleCrtcs.First().crtc_id; + } + + throw new InvalidOperationException("Unable to find CRTC matching the desired mode"); + } + } + + _crtcId = GetCrtc(); + var device = gbm_create_device(card.Fd); + _gbmTargetSurface = gbm_surface_create(device, modeInfo.Resolution.Width, modeInfo.Resolution.Height, + GbmColorFormats.GBM_FORMAT_XRGB8888, GbmBoFlags.GBM_BO_USE_SCANOUT | GbmBoFlags.GBM_BO_USE_RENDERING); + if(_gbmTargetSurface == null) + throw new InvalidOperationException("Unable to create GBM surface"); + + + + _eglDisplay = new EglDisplay(new EglInterface(eglGetProcAddress), 0x31D7, device, null); + _eglSurface = _eglDisplay.CreateWindowSurface(_gbmTargetSurface); + + + EglContext CreateContext(EglContext share) + { + var offSurf = gbm_surface_create(device, 1, 1, GbmColorFormats.GBM_FORMAT_XRGB8888, + GbmBoFlags.GBM_BO_USE_RENDERING); + if (offSurf == null) + throw new InvalidOperationException("Unable to create 1x1 sized GBM surface"); + return _eglDisplay.CreateContext(share, _eglDisplay.CreateWindowSurface(offSurf)); + } + + _immediateContext = CreateContext(null); + _deferredContext = CreateContext(_immediateContext); + + _immediateContext.MakeCurrent(_eglSurface); + _eglDisplay.GlInterface.ClearColor(0, 0, 0, 0); + _eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT); + _eglSurface.SwapBuffers(); + var bo = gbm_surface_lock_front_buffer(_gbmTargetSurface); + var fbId = GetFbIdForBo(bo); + var connectorId = connector.Id; + var mode = modeInfo.Mode; + + + var res = drmModeSetCrtc(_card.Fd, _crtcId, fbId, 0, 0, &connectorId, 1, &mode); + if (res != 0) + throw new Win32Exception(res, "drmModeSetCrtc failed"); + + _mode = mode; + _currentBo = bo; + + // Go trough two cycles of buffer swapping (there are render artifacts otherwise) + for(var c=0;c<2;c++) + using (CreateGlRenderTarget().BeginDraw()) + { + _eglDisplay.GlInterface.ClearColor(0, 0, 0, 0); + _eglDisplay.GlInterface.Clear(GlConsts.GL_COLOR_BUFFER_BIT | GlConsts.GL_STENCIL_BUFFER_BIT); + } + } + + public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget() + { + return new RenderTarget(this); + } + + class RenderTarget : IGlPlatformSurfaceRenderTarget + { + private readonly DrmOutput _parent; + + public RenderTarget(DrmOutput parent) + { + _parent = parent; + } + public void Dispose() + { + // We are wrapping GBM buffer chain associated with CRTC, and don't free it on a whim + } + + class RenderSession : IGlPlatformSurfaceRenderingSession + { + private readonly DrmOutput _parent; + + public RenderSession(DrmOutput parent) + { + _parent = parent; + } + + public void Dispose() + { + _parent._eglDisplay.GlInterface.Flush(); + _parent._eglSurface.SwapBuffers(); + + var nextBo = gbm_surface_lock_front_buffer(_parent._gbmTargetSurface); + if (nextBo == IntPtr.Zero) + { + // Not sure what else can be done + Console.WriteLine("gbm_surface_lock_front_buffer failed"); + } + else + { + + var fb = _parent.GetFbIdForBo(nextBo); + bool waitingForFlip = true; + + drmModePageFlip(_parent._card.Fd, _parent._crtcId, fb, DrmModePageFlip.Event, null); + + DrmEventPageFlipHandlerDelegate flipCb = + (int fd, uint sequence, uint tv_sec, uint tv_usec, void* user_data) => + { + waitingForFlip = false; + }; + var cbHandle = GCHandle.Alloc(flipCb); + var ctx = new DrmEventContext + { + version = 4, page_flip_handler = Marshal.GetFunctionPointerForDelegate(flipCb) + }; + while (waitingForFlip) + { + var pfd = new pollfd {events = 1, fd = _parent._card.Fd}; + poll(&pfd, new IntPtr(1), -1); + drmHandleEvent(_parent._card.Fd, &ctx); + } + + cbHandle.Free(); + gbm_surface_release_buffer(_parent._gbmTargetSurface, _parent._currentBo); + _parent._currentBo = nextBo; + } + _parent._eglDisplay.ClearContext(); + } + + + public IGlDisplay Display => _parent._eglDisplay; + + public PixelSize Size => _parent._mode.Resolution; + + public double Scaling => 1; + } + + public IGlPlatformSurfaceRenderingSession BeginDraw() + { + _parent._deferredContext.MakeCurrent(_parent._eglSurface); + return new RenderSession(_parent); + } + } + + IGlContext IWindowingPlatformGlFeature.ImmediateContext => _immediateContext; + } + + +} diff --git a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs similarity index 92% rename from src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs rename to src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs index 1e25bd4a8a..3021c29015 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/LinuxFramebuffer.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/FbdevOutput.cs @@ -2,11 +2,12 @@ using System.Runtime.InteropServices; using System.Text; using Avalonia.Controls.Platform.Surfaces; +using Avalonia.LinuxFramebuffer.Output; using Avalonia.Platform; namespace Avalonia.LinuxFramebuffer { - public sealed unsafe class LinuxFramebuffer : IFramebufferPlatformSurface, IDisposable + public sealed unsafe class FbdevOutput : IFramebufferPlatformSurface, IDisposable, IOutputBackend { private readonly Vector _dpi; private int _fd; @@ -15,7 +16,7 @@ namespace Avalonia.LinuxFramebuffer private IntPtr _mappedLength; private IntPtr _mappedAddress; - public LinuxFramebuffer(string fileName = null, Vector? dpi = null) + public FbdevOutput(string fileName = null, Vector? dpi = null) { _dpi = dpi ?? new Vector(96, 96); fileName = fileName ?? Environment.GetEnvironmentVariable("FRAMEBUFFER") ?? "/dev/fb0"; @@ -85,14 +86,14 @@ namespace Avalonia.LinuxFramebuffer public string Id { get; private set; } - public Size PixelSize + public PixelSize PixelSize { get { fb_var_screeninfo nfo; if (-1 == NativeUnsafeMethods.ioctl(_fd, FbIoCtl.FBIOGET_VSCREENINFO, &nfo)) throw new Exception("FBIOGET_VSCREENINFO error: " + Marshal.GetLastWin32Error()); - return new Size(nfo.xres, nfo.yres); + return new PixelSize((int)nfo.xres, (int)nfo.yres); } } @@ -123,7 +124,7 @@ namespace Avalonia.LinuxFramebuffer GC.SuppressFinalize(this); } - ~LinuxFramebuffer() + ~FbdevOutput() { ReleaseUnmanagedResources(); } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs new file mode 100644 index 0000000000..01690f07ac --- /dev/null +++ b/src/Linux/Avalonia.LinuxFramebuffer/Output/IOutputBackend.cs @@ -0,0 +1,7 @@ +namespace Avalonia.LinuxFramebuffer.Output +{ + public interface IOutputBackend + { + PixelSize PixelSize { get; } + } +}