Skip to content
1 change: 1 addition & 0 deletions src/SamplesApp/SamplesApp.Shared/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,7 @@ public static void ConfigureLogging()

// Display Skia related information
builder.AddFilter("Uno.UI.Runtime.Skia", LogLevel.Debug);
builder.AddFilter("Uno.WinUI.Runtime.Skia", LogLevel.Debug);
builder.AddFilter("Uno.UI.Skia", LogLevel.Debug);

// builder.AddFilter("Uno.UI.Runtime.Skia", LogLevel.Trace);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,9 @@ public void HandleXI2Event(IntPtr display, XEvent ev)
case XiEventType.XI_DeviceChanged:
{
var data = ev.GenericEventCookie.GetEvent<XIDeviceEvent>();
if (this.Log().IsEnabled(LogLevel.Error))
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Error($"XI2 {evtype} EVENT: {data.event_x}x{data.event_y} {data.buttons}");
this.Log().Trace($"XI2 {evtype} EVENT: {data.event_x}x{data.event_y} {data.buttons}");
}
_deviceInfoCache.Remove(data.sourceid);
_valuatorValues.Remove(data.sourceid);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@
using Windows.Graphics.Display;
using Uno.Disposables;
using Uno.Foundation.Logging;
using Uno.UI;

namespace Uno.WinUI.Runtime.Skia.X11
{
Expand Down Expand Up @@ -486,8 +487,17 @@ private unsafe (DisplayInformationDetails details, double? fps)? GetDisplayInfor

// xrandr does something similar to this, except it reads the XRRModeInfo from outputInfo->modes by picking
// a "best fit" mode and reading it, but this can actually result in dotClock == 0 in some cases and this
// show when calling xrandr. Reading from the CRTC info struct returns a more accurate result
var fps = mode_refresh((X11Helper.XRRModeInfo*)&crtcInfo->mode);
// shows when calling xrandr. Reading from the CRTC info struct returns a more accurate result
var modeSpan = new Span<X11Helper.XRRModeInfo>((void*)resources->modes, resources->nmode);
var fps = FeatureConfiguration.CompositionTarget.FrameRate;
foreach (var testMode in modeSpan)
{
if (crtcInfo->mode == testMode.id && mode_refresh(&testMode) is { } testFps)
{
fps = (float)testFps;
break;
}
}

return (new DisplayInformationDetails(
rawWidth,
Expand Down
90 changes: 66 additions & 24 deletions src/Uno.UI.Runtime.Skia.X11/Hosting/X11XamlRootHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -372,9 +372,62 @@ private void Initialize()
IntPtr rootXWindow = XLib.XRootWindow(display, screen);
_x11Window = CreateSoftwareRenderWindow(display, screen, size, rootXWindow);
var topWindowDisplay = XLib.XOpenDisplay(IntPtr.Zero);
_x11TopWindow = FeatureConfiguration.Rendering.UseOpenGLOnX11 ?? IsOpenGLSupported(display)
? CreateGLXWindow(topWindowDisplay, screen, size, RootX11Window.Window)
: CreateSoftwareRenderWindow(topWindowDisplay, screen, size, RootX11Window.Window);
if (FeatureConfiguration.Rendering.UseOpenGLOnX11 ?? true)
{
try
{
if (FeatureConfiguration.Rendering.PreferGLESOverGLOnX11)
{
_x11TopWindow = CreateSoftwareRenderWindow(topWindowDisplay, screen, size, RootX11Window.Window);
_renderer = new X11EGLRenderer(this, TopX11Window);
}
else
{
_x11TopWindow = CreateGLXWindow(topWindowDisplay, screen, size, RootX11Window.Window);
_ = XLib.XSync(display, false);
_renderer = new X11OpenGLRenderer(this, TopX11Window);
}
}
catch (Exception e)
{
if (_x11TopWindow is not null)
{
_ = XLib.XDestroyWindow(_x11TopWindow.Value.Display, _x11TopWindow.Value.Window);
_x11TopWindow = null;
}
try
{
if (FeatureConfiguration.Rendering.PreferGLESOverGLOnX11)
{
_x11TopWindow = CreateGLXWindow(topWindowDisplay, screen, size, RootX11Window.Window);
_ = XLib.XSync(display, false);
_renderer = new X11OpenGLRenderer(this, TopX11Window);
}
else
{
this.Log().Info($"Attempted to create a GLX OpenGL context but failed with '{e.Message}'. Falling back to an EGL OpenGL ES context.");
_x11TopWindow = CreateSoftwareRenderWindow(topWindowDisplay, screen, size, RootX11Window.Window);
_ = XLib.XSync(display, false);
_renderer = new X11EGLRenderer(this, TopX11Window);
}
}
catch (Exception e2)
{
this.Log().Info($"Second attempt at creating and OpenGL / OpenGL ES context failed with '{e2.Message}'. Falling back to software rendering.");
if (_x11TopWindow is null)
{
_x11TopWindow = CreateSoftwareRenderWindow(topWindowDisplay, screen, size, RootX11Window.Window);
}
_renderer = new X11SoftwareRenderer(this, TopX11Window);
}
}
}
else
{
this.Log().Info($"Forcing software rendering.");
_x11TopWindow = CreateSoftwareRenderWindow(topWindowDisplay, screen, size, RootX11Window.Window);
_renderer = new X11SoftwareRenderer(this, TopX11Window);
}

// Only XI2.2 has touch events, and that's pretty much the only reason we're using XI2,
// so to make our assumptions simpler, we assume XI >= 2.2 or no XI at all.
Expand Down Expand Up @@ -406,21 +459,17 @@ private void Initialize()
}

_ = X11Helper.XClearWindow(RootX11Window.Display, RootX11Window.Window); // the root window is never drawn, just always blank

if (FeatureConfiguration.Rendering.UseOpenGLOnX11 ?? IsOpenGLSupported(TopX11Window.Display))
{
_renderer = new X11OpenGLRenderer(this, TopX11Window);
}
else
{
_renderer = new X11SoftwareRenderer(this, TopX11Window);
}
}

// https://github.com/gamedevtech/X11OpenGLWindow/blob/4a3d55bb7aafd135670947f71bd2a3ee691d3fb3/README.md
// https://learnopengl.com/Advanced-OpenGL/Framebuffers
private unsafe static X11Window CreateGLXWindow(IntPtr display, int screen, Size size, IntPtr parent)
{
if (!GlxInterface.glXQueryExtension(display, out _, out _))
{
throw new InvalidOperationException($"{nameof(GlxInterface.glXQueryExtension)} returned false");
}

IntPtr bestFbc = IntPtr.Zero;
XVisualInfo* visual = null;
var ptr = GlxInterface.glXChooseFBConfig(display, screen, _glxAttribs, out var count);
Expand All @@ -445,6 +494,10 @@ private unsafe static X11Window CreateGLXWindow(IntPtr display, int screen, Size
}

IntPtr context = GlxInterface.glXCreateNewContext(display, bestFbc, GlxConsts.GLX_RGBA_TYPE, IntPtr.Zero, /* True */ 1);
if (context == IntPtr.Zero)
{
throw new InvalidOperationException($"{nameof(GlxInterface.glXCreateNewContext)} failed and returned a null context.\n");
}
_ = XLib.XSync(display, false);

XSetWindowAttributes attribs = default;
Expand Down Expand Up @@ -507,21 +560,10 @@ private static X11Window CreateSoftwareRenderWindow(IntPtr display, int screen,
var window = XLib.XCreateWindow(display, parent, 0, 0, (int)size.Width,
(int)size.Height, 0, (int)depth, /* InputOutput */ 1, visual,
(UIntPtr)(valueMask), ref xSetWindowAttributes);
_ = XLib.XSync(display, false);
return new X11Window(display, window);
}

private bool IsOpenGLSupported(IntPtr display)
{
try
{
return GlxInterface.glXQueryExtension(display, out _, out _);
}
catch (Exception) // most likely DllNotFoundException, but can be other types
{
return false;
}
}

UIElement? IXamlRootHost.RootElement => _window.RootElement;

private void RaiseConfigureCallback()
Expand Down
159 changes: 159 additions & 0 deletions src/Uno.UI.Runtime.Skia.X11/Rendering/X11EGLRenderer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
ο»Ώusing System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using SkiaSharp;
using Uno.Disposables;
using Uno.Foundation.Logging;
using Uno.UI.Helpers;
using Uno.UI.Hosting;

namespace Uno.WinUI.Runtime.Skia.X11
{
internal class X11EGLRenderer : X11Renderer, IDisposable
{
private const uint DefaultFramebuffer = 0; // this is the glX buffer that was created in X11XamlRootHost, which will directly render on screen

private readonly GRContext _grContext;
private readonly IntPtr _eglDisplay;
private readonly IntPtr _glContext;
private readonly IntPtr _eglSurface;
private readonly int _samples;
private readonly int _stencil;

private GRBackendRenderTarget? _renderTarget;
private IDisposable? _contextCurrentDisposable;

public unsafe X11EGLRenderer(IXamlRootHost host, X11Window x11window) : base(host, x11window)
{
_eglDisplay = EglHelper.EglGetDisplay(x11window.Display);
EglHelper.EglInitialize(_eglDisplay, out var major, out var minor);
if (this.Log().IsEnabled(LogLevel.Information))
{
this.Log().Info($"Found EGL version {major}.{minor}.");
}

int[] attribList =
{
EglHelper.EGL_RED_SIZE, 8,
EglHelper.EGL_GREEN_SIZE, 8,
EglHelper.EGL_BLUE_SIZE, 8,
EglHelper.EGL_ALPHA_SIZE, 8,
EglHelper.EGL_DEPTH_SIZE, 8,
EglHelper.EGL_STENCIL_SIZE, 1,
EglHelper.EGL_RENDERABLE_TYPE, EglHelper.EGL_OPENGL_ES2_BIT,
EglHelper.EGL_NONE
};

var configs = new IntPtr[1];
var success = EglHelper.EglChooseConfig(_eglDisplay, attribList, configs, configs.Length, out var numConfig);

if (!success || numConfig < 1)
{
throw new InvalidOperationException($"{nameof(EglHelper.EglChooseConfig)} failed: {Enum.GetName(EglHelper.EglGetError())}");
}

if (!EglHelper.EglGetConfigAttrib(_eglDisplay, configs[0], EglHelper.EGL_SAMPLES, out _samples))
{
throw new InvalidOperationException($"{nameof(EglHelper.EglGetConfigAttrib)} failed to get {nameof(EglHelper.EGL_SAMPLES)}: {Enum.GetName(EglHelper.EglGetError())}");
}
if (!EglHelper.EglGetConfigAttrib(_eglDisplay, configs[0], EglHelper.EGL_STENCIL_SIZE, out _stencil))
{
throw new InvalidOperationException($"{nameof(EglHelper.EglGetConfigAttrib)} failed to get {nameof(EglHelper.EGL_STENCIL_SIZE)}: {Enum.GetName(EglHelper.EglGetError())}");
}

// ANGLE implements GLES 3
_glContext = EglHelper.EglCreateContext(_eglDisplay, configs[0], EglHelper.EGL_NO_CONTEXT, [EglHelper.EGL_CONTEXT_CLIENT_VERSION, 2, EglHelper.EGL_NONE]);
if (_glContext == IntPtr.Zero)
{
throw new InvalidOperationException($"EGL context creation failed: {Enum.GetName(EglHelper.EglGetError())}");
}

var window = x11window.Window;
var windowPtr = new IntPtr(&window);
_eglSurface = EglHelper.EglCreatePlatformWindowSurface(_eglDisplay, configs[0], windowPtr, [EglHelper.EGL_NONE]);

MakeCurrent();

var glInterface = GRGlInterface.CreateGles(EglHelper.EglGetProcAddress);

if (glInterface == null)
{
throw new NotSupportedException($"OpenGL is not supported in this system");
}

var context = GRContext.CreateGl(glInterface);

if (context == null)
{
throw new NotSupportedException($"OpenGL is not supported in this system (failed to create context)");
}

var glGetString = (delegate* unmanaged[Cdecl]<int, byte*>)EglHelper.EglGetProcAddress("glGetString");

var glVersionBytePtr = glGetString(/* GL_VERSION */ 0x1F02);
var glVersionString = Marshal.PtrToStringUTF8((IntPtr)glVersionBytePtr);

if (this.Log().IsEnabled(LogLevel.Information))
{
this.Log().Info($"Using {glVersionString} for rendering.");
}

_contextCurrentDisposable!.Dispose();

_grContext = context;
}

public void Dispose() => _grContext.Dispose();

protected override SKSurface UpdateSize(int width, int height, int depth)
{
_renderTarget?.Dispose();

var skColorType = SKColorType.Rgba8888; // this is Rgba8888 regardless of SKImageInfo.PlatformColorType
var grSurfaceOrigin = GRSurfaceOrigin.BottomLeft; // to match OpenGL's origin

var glInfo = new GRGlFramebufferInfo(DefaultFramebuffer, skColorType.ToGlSizedFormat());

_renderTarget = new GRBackendRenderTarget(width, height, _samples, _stencil, glInfo);
return SKSurface.Create(_grContext, _renderTarget, grSurfaceOrigin, skColorType);
}

protected override void MakeCurrent()
{
var glContext = EglHelper.EglGetCurrentContext();
var readSurface = EglHelper.EglGetCurrentSurface(EglHelper.EGL_READ);
var drawSurface = EglHelper.EglGetCurrentSurface(EglHelper.EGL_DRAW);
if (!EglHelper.EglMakeCurrent(_eglDisplay, _eglSurface, _eglSurface, _glContext))
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"{nameof(EglHelper.EglMakeCurrent)} failed.");
}
}
_contextCurrentDisposable = Disposable.Create(() =>
{
if (!EglHelper.EglMakeCurrent(_eglDisplay, drawSurface, readSurface, glContext))
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"{nameof(EglHelper.EglMakeCurrent)} failed.");
}
}
});
}

protected override void Flush()
{
if (!EglHelper.EglSwapBuffers(_eglDisplay, _eglSurface))
{
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"{nameof(EglHelper.EglSwapBuffers)} failed.");
}
}
Debug.Assert(_contextCurrentDisposable is not null);
_contextCurrentDisposable?.Dispose();
_contextCurrentDisposable = null;
}
}
}
13 changes: 12 additions & 1 deletion src/Uno.UI.Runtime.Skia.X11/Rendering/X11OpenGLRenderer.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
ο»Ώusing System;
using System.Globalization;
using System.Runtime.InteropServices;
using SkiaSharp;
using Uno.Disposables;
using Uno.Foundation.Logging;
Expand Down Expand Up @@ -57,7 +58,7 @@ protected override void Flush()
}
}

private GRContext CreateGRGLContext()
private unsafe GRContext CreateGRGLContext()
{
if (_x11Window.glXInfo is not { } glXInfo)
{
Expand Down Expand Up @@ -90,6 +91,16 @@ private GRContext CreateGRGLContext()
throw new NotSupportedException($"OpenGL is not supported in this system (failed to create context)");
}

var glGetString = (delegate* unmanaged[Cdecl]<int, byte*>)GlxInterface.glXGetProcAddress("glGetString");

var glVersionBytePtr = glGetString(/* GL_VERSION */ 0x1F02);
var glVersionString = Marshal.PtrToStringUTF8((IntPtr)glVersionBytePtr);

if (this.Log().IsEnabled(LogLevel.Information))
{
this.Log().Info($"Using OpenGL {glVersionString} for rendering.");
}

return context;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Diagnostics;
using Uno.UI;
using Uno.UI.Dispatching;
using Uno.UI.Hosting;
using Timer = System.Timers.Timer;

Expand Down
7 changes: 7 additions & 0 deletions src/Uno.UI/FeatureConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -885,6 +885,13 @@ public static class Rendering
/// </summary>
public static bool? UseOpenGLOnX11 { get; set; }

/// <summary>
/// Determines if OpenGL ES + EGL should be used instead of OpenGL + GLX if both are available. This value is only
/// used if <see cref="UseOpenGLOnX11"/> is true or null. This property only affects the order of attempting
/// to create a GL/GlES context but even when true, if the preferred API fails, the other will be attempted.
/// </summary>
public static bool PreferGLESOverGLOnX11 { get; set; }

/// <summary>
/// Determines if OpenGL rendering should be enabled on the Win32 target. If null, defaults to
/// OpenGL if available. Otherwise, software rendering will be used.
Expand Down
Loading
Loading