// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.InteropServices; using SkiaSharp; using Microsoft.Maui.Platform.Linux.Window; using Microsoft.Maui.Platform.Linux.Rendering; namespace Microsoft.Maui.Platform.Linux.Services; /// /// Supported display server types. /// public enum DisplayServerType { Auto, X11, Wayland } /// /// Factory for creating display server connections. /// Supports X11 and Wayland display servers. /// public static class DisplayServerFactory { private static DisplayServerType? _cachedServerType; /// /// Detects the current display server type. /// public static DisplayServerType DetectDisplayServer() { if (_cachedServerType.HasValue) return _cachedServerType.Value; // Check for Wayland first (modern default) var waylandDisplay = Environment.GetEnvironmentVariable("WAYLAND_DISPLAY"); if (!string.IsNullOrEmpty(waylandDisplay)) { // Check if XWayland is available - prefer it for now until native Wayland is fully tested var xDisplay = Environment.GetEnvironmentVariable("DISPLAY"); var preferX11 = Environment.GetEnvironmentVariable("MAUI_PREFER_X11"); if (!string.IsNullOrEmpty(xDisplay) && !string.IsNullOrEmpty(preferX11)) { Console.WriteLine("[DisplayServer] XWayland detected, using X11 backend (MAUI_PREFER_X11 set)"); _cachedServerType = DisplayServerType.X11; return DisplayServerType.X11; } Console.WriteLine("[DisplayServer] Wayland display detected"); _cachedServerType = DisplayServerType.Wayland; return DisplayServerType.Wayland; } // Fall back to X11 var x11Display = Environment.GetEnvironmentVariable("DISPLAY"); if (!string.IsNullOrEmpty(x11Display)) { Console.WriteLine("[DisplayServer] X11 display detected"); _cachedServerType = DisplayServerType.X11; return DisplayServerType.X11; } // Default to X11 and let it fail if not available Console.WriteLine("[DisplayServer] No display server detected, defaulting to X11"); _cachedServerType = DisplayServerType.X11; return DisplayServerType.X11; } /// /// Creates a window for the specified or detected display server. /// public static IDisplayWindow CreateWindow(string title, int width, int height, DisplayServerType serverType = DisplayServerType.Auto) { if (serverType == DisplayServerType.Auto) { serverType = DetectDisplayServer(); } return serverType switch { DisplayServerType.X11 => CreateX11Window(title, width, height), DisplayServerType.Wayland => CreateWaylandWindow(title, width, height), _ => CreateX11Window(title, width, height) }; } private static IDisplayWindow CreateX11Window(string title, int width, int height) { try { Console.WriteLine($"[DisplayServer] Creating X11 window: {title} ({width}x{height})"); return new X11DisplayWindow(title, width, height); } catch (Exception ex) { Console.WriteLine($"[DisplayServer] Failed to create X11 window: {ex.Message}"); throw; } } private static IDisplayWindow CreateWaylandWindow(string title, int width, int height) { try { Console.WriteLine($"[DisplayServer] Creating Wayland window: {title} ({width}x{height})"); return new WaylandDisplayWindow(title, width, height); } catch (Exception ex) { Console.WriteLine($"[DisplayServer] Failed to create Wayland window: {ex.Message}"); // Try to fall back to X11 via XWayland var xDisplay = Environment.GetEnvironmentVariable("DISPLAY"); if (!string.IsNullOrEmpty(xDisplay)) { Console.WriteLine("[DisplayServer] Falling back to X11 (XWayland)"); return CreateX11Window(title, width, height); } throw; } } /// /// Gets a human-readable name for the display server. /// public static string GetDisplayServerName(DisplayServerType serverType = DisplayServerType.Auto) { if (serverType == DisplayServerType.Auto) serverType = DetectDisplayServer(); return serverType switch { DisplayServerType.X11 => "X11", DisplayServerType.Wayland => "Wayland", _ => "Unknown" }; } } /// /// Common interface for display server windows. /// public interface IDisplayWindow : IDisposable { int Width { get; } int Height { get; } bool IsRunning { get; } void Show(); void Hide(); void SetTitle(string title); void Resize(int width, int height); void ProcessEvents(); void Stop(); event EventHandler? KeyDown; event EventHandler? KeyUp; event EventHandler? TextInput; event EventHandler? PointerMoved; event EventHandler? PointerPressed; event EventHandler? PointerReleased; event EventHandler? Scroll; event EventHandler? Exposed; event EventHandler<(int Width, int Height)>? Resized; event EventHandler? CloseRequested; } /// /// X11 display window wrapper implementing the common interface. /// public class X11DisplayWindow : IDisplayWindow { private readonly X11Window _window; public int Width => _window.Width; public int Height => _window.Height; public bool IsRunning => _window.IsRunning; public event EventHandler? KeyDown; public event EventHandler? KeyUp; public event EventHandler? TextInput; public event EventHandler? PointerMoved; public event EventHandler? PointerPressed; public event EventHandler? PointerReleased; public event EventHandler? Scroll; public event EventHandler? Exposed; public event EventHandler<(int Width, int Height)>? Resized; public event EventHandler? CloseRequested; public X11DisplayWindow(string title, int width, int height) { _window = new X11Window(title, width, height); _window.KeyDown += (s, e) => KeyDown?.Invoke(this, e); _window.KeyUp += (s, e) => KeyUp?.Invoke(this, e); _window.TextInput += (s, e) => TextInput?.Invoke(this, e); _window.PointerMoved += (s, e) => PointerMoved?.Invoke(this, e); _window.PointerPressed += (s, e) => PointerPressed?.Invoke(this, e); _window.PointerReleased += (s, e) => PointerReleased?.Invoke(this, e); _window.Scroll += (s, e) => Scroll?.Invoke(this, e); _window.Exposed += (s, e) => Exposed?.Invoke(this, e); _window.Resized += (s, e) => Resized?.Invoke(this, e); _window.CloseRequested += (s, e) => CloseRequested?.Invoke(this, e); } public void Show() => _window.Show(); public void Hide() => _window.Hide(); public void SetTitle(string title) => _window.SetTitle(title); public void Resize(int width, int height) => _window.Resize(width, height); public void ProcessEvents() => _window.ProcessEvents(); public void Stop() => _window.Stop(); public void Dispose() => _window.Dispose(); } /// /// Wayland display window wrapper implementing IDisplayWindow. /// Uses wl_shm for software rendering with SkiaSharp. /// public class WaylandDisplayWindow : IDisplayWindow { #region Native Interop private const string LibWaylandClient = "libwayland-client.so.0"; [DllImport(LibWaylandClient)] private static extern IntPtr wl_display_connect(string? name); [DllImport(LibWaylandClient)] private static extern void wl_display_disconnect(IntPtr display); [DllImport(LibWaylandClient)] private static extern int wl_display_dispatch(IntPtr display); [DllImport(LibWaylandClient)] private static extern int wl_display_dispatch_pending(IntPtr display); [DllImport(LibWaylandClient)] private static extern int wl_display_roundtrip(IntPtr display); [DllImport(LibWaylandClient)] private static extern int wl_display_flush(IntPtr display); [DllImport(LibWaylandClient)] private static extern IntPtr wl_display_get_registry(IntPtr display); [DllImport(LibWaylandClient)] private static extern IntPtr wl_compositor_create_surface(IntPtr compositor); [DllImport(LibWaylandClient)] private static extern void wl_surface_attach(IntPtr surface, IntPtr buffer, int x, int y); [DllImport(LibWaylandClient)] private static extern void wl_surface_damage(IntPtr surface, int x, int y, int width, int height); [DllImport(LibWaylandClient)] private static extern void wl_surface_commit(IntPtr surface); [DllImport(LibWaylandClient)] private static extern void wl_surface_destroy(IntPtr surface); [DllImport(LibWaylandClient)] private static extern IntPtr wl_shm_create_pool(IntPtr shm, int fd, int size); [DllImport(LibWaylandClient)] private static extern void wl_shm_pool_destroy(IntPtr pool); [DllImport(LibWaylandClient)] private static extern IntPtr wl_shm_pool_create_buffer(IntPtr pool, int offset, int width, int height, int stride, uint format); [DllImport(LibWaylandClient)] private static extern void wl_buffer_destroy(IntPtr buffer); [DllImport("libc", EntryPoint = "shm_open")] private static extern int shm_open([MarshalAs(UnmanagedType.LPStr)] string name, int oflag, int mode); [DllImport("libc", EntryPoint = "shm_unlink")] private static extern int shm_unlink([MarshalAs(UnmanagedType.LPStr)] string name); [DllImport("libc", EntryPoint = "ftruncate")] private static extern int ftruncate(int fd, long length); [DllImport("libc", EntryPoint = "mmap")] private static extern IntPtr mmap(IntPtr addr, nuint length, int prot, int flags, int fd, long offset); [DllImport("libc", EntryPoint = "munmap")] private static extern int munmap(IntPtr addr, nuint length); [DllImport("libc", EntryPoint = "close")] private static extern int close(int fd); private const int O_RDWR = 2; private const int O_CREAT = 0x40; private const int O_EXCL = 0x80; private const int PROT_READ = 1; private const int PROT_WRITE = 2; private const int MAP_SHARED = 1; private const uint WL_SHM_FORMAT_XRGB8888 = 1; #endregion private IntPtr _display; private IntPtr _registry; private IntPtr _compositor; private IntPtr _shm; private IntPtr _surface; private IntPtr _shmPool; private IntPtr _buffer; private IntPtr _pixelData; private int _shmFd = -1; private int _bufferSize; private int _width; private int _height; private string _title; private bool _isRunning; private bool _disposed; private SKBitmap? _bitmap; private SKCanvas? _canvas; public int Width => _width; public int Height => _height; public bool IsRunning => _isRunning; public event EventHandler? KeyDown; public event EventHandler? KeyUp; public event EventHandler? TextInput; public event EventHandler? PointerMoved; public event EventHandler? PointerPressed; public event EventHandler? PointerReleased; public event EventHandler? Scroll; public event EventHandler? Exposed; public event EventHandler<(int Width, int Height)>? Resized; public event EventHandler? CloseRequested; public WaylandDisplayWindow(string title, int width, int height) { _title = title; _width = width; _height = height; Initialize(); } private void Initialize() { _display = wl_display_connect(null); if (_display == IntPtr.Zero) { throw new InvalidOperationException("Failed to connect to Wayland display. Is WAYLAND_DISPLAY set?"); } _registry = wl_display_get_registry(_display); if (_registry == IntPtr.Zero) { throw new InvalidOperationException("Failed to get Wayland registry"); } // Note: A full implementation would set up registry listeners to get // compositor and shm handles. For now, we throw an informative error // and fall back to X11 via XWayland in DisplayServerFactory. // This is a placeholder - proper Wayland support requires: // 1. Setting up wl_registry_listener with callbacks // 2. Binding to wl_compositor, wl_shm, wl_seat, xdg_wm_base // 3. Implementing the xdg-shell protocol for toplevel windows wl_display_roundtrip(_display); // For now, signal that native Wayland isn't fully implemented throw new NotSupportedException( "Native Wayland support is experimental. " + "Set MAUI_PREFER_X11=1 to use XWayland, or run with DISPLAY set."); } private void CreateShmBuffer() { int stride = _width * 4; _bufferSize = stride * _height; string shmName = $"/maui-shm-{Environment.ProcessId}-{DateTime.Now.Ticks}"; _shmFd = shm_open(shmName, O_RDWR | O_CREAT | O_EXCL, 0600); if (_shmFd < 0) { throw new InvalidOperationException("Failed to create shared memory file"); } shm_unlink(shmName); if (ftruncate(_shmFd, _bufferSize) < 0) { close(_shmFd); throw new InvalidOperationException("Failed to resize shared memory"); } _pixelData = mmap(IntPtr.Zero, (nuint)_bufferSize, PROT_READ | PROT_WRITE, MAP_SHARED, _shmFd, 0); if (_pixelData == IntPtr.Zero || _pixelData == new IntPtr(-1)) { close(_shmFd); throw new InvalidOperationException("Failed to mmap shared memory"); } _shmPool = wl_shm_create_pool(_shm, _shmFd, _bufferSize); if (_shmPool == IntPtr.Zero) { munmap(_pixelData, (nuint)_bufferSize); close(_shmFd); throw new InvalidOperationException("Failed to create wl_shm_pool"); } _buffer = wl_shm_pool_create_buffer(_shmPool, 0, _width, _height, stride, WL_SHM_FORMAT_XRGB8888); if (_buffer == IntPtr.Zero) { wl_shm_pool_destroy(_shmPool); munmap(_pixelData, (nuint)_bufferSize); close(_shmFd); throw new InvalidOperationException("Failed to create wl_buffer"); } // Create Skia bitmap backed by shared memory var info = new SKImageInfo(_width, _height, SKColorType.Bgra8888, SKAlphaType.Opaque); _bitmap = new SKBitmap(); _bitmap.InstallPixels(info, _pixelData, stride); _canvas = new SKCanvas(_bitmap); } public void Show() { if (_surface == IntPtr.Zero || _buffer == IntPtr.Zero) return; wl_surface_attach(_surface, _buffer, 0, 0); wl_surface_damage(_surface, 0, 0, _width, _height); wl_surface_commit(_surface); wl_display_flush(_display); } public void Hide() { if (_surface == IntPtr.Zero) return; wl_surface_attach(_surface, IntPtr.Zero, 0, 0); wl_surface_commit(_surface); wl_display_flush(_display); } public void SetTitle(string title) { _title = title; } public void Resize(int width, int height) { if (width == _width && height == _height) return; _canvas?.Dispose(); _bitmap?.Dispose(); if (_buffer != IntPtr.Zero) wl_buffer_destroy(_buffer); if (_shmPool != IntPtr.Zero) wl_shm_pool_destroy(_shmPool); if (_pixelData != IntPtr.Zero) munmap(_pixelData, (nuint)_bufferSize); if (_shmFd >= 0) close(_shmFd); _width = width; _height = height; CreateShmBuffer(); Resized?.Invoke(this, (width, height)); } public void ProcessEvents() { if (!_isRunning || _display == IntPtr.Zero) return; wl_display_dispatch_pending(_display); wl_display_flush(_display); } public void Stop() { _isRunning = false; } public SKCanvas? GetCanvas() => _canvas; public void CommitFrame() { if (_surface != IntPtr.Zero && _buffer != IntPtr.Zero) { wl_surface_attach(_surface, _buffer, 0, 0); wl_surface_damage(_surface, 0, 0, _width, _height); wl_surface_commit(_surface); wl_display_flush(_display); } } public void Dispose() { if (_disposed) return; _disposed = true; _isRunning = false; _canvas?.Dispose(); _bitmap?.Dispose(); if (_buffer != IntPtr.Zero) { wl_buffer_destroy(_buffer); _buffer = IntPtr.Zero; } if (_shmPool != IntPtr.Zero) { wl_shm_pool_destroy(_shmPool); _shmPool = IntPtr.Zero; } if (_pixelData != IntPtr.Zero && _pixelData != new IntPtr(-1)) { munmap(_pixelData, (nuint)_bufferSize); _pixelData = IntPtr.Zero; } if (_shmFd >= 0) { close(_shmFd); _shmFd = -1; } if (_surface != IntPtr.Zero) { wl_surface_destroy(_surface); _surface = IntPtr.Zero; } if (_display != IntPtr.Zero) { wl_display_disconnect(_display); _display = IntPtr.Zero; } } }