diff --git a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj index fbe6a77974..887e375877 100644 --- a/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj +++ b/src/Avalonia.OpenGL/Avalonia.OpenGL.csproj @@ -10,6 +10,10 @@ + + + + diff --git a/src/Avalonia.OpenGL/Egl/EglConsts.cs b/src/Avalonia.OpenGL/Egl/EglConsts.cs index b4fe8a99eb..295acef44b 100644 --- a/src/Avalonia.OpenGL/Egl/EglConsts.cs +++ b/src/Avalonia.OpenGL/Egl/EglConsts.cs @@ -35,7 +35,7 @@ namespace Avalonia.OpenGL.Egl // public const int EGL_MAX_PBUFFER_PIXELS = 0x302B; // public const int EGL_MAX_PBUFFER_WIDTH = 0x302C; // public const int EGL_NATIVE_RENDERABLE = 0x302D; -// public const int EGL_NATIVE_VISUAL_ID = 0x302E; + public const int EGL_NATIVE_VISUAL_ID = 0x302E; // public const int EGL_NATIVE_VISUAL_TYPE = 0x302F; public const int EGL_NONE = 0x3038; // public const int EGL_NON_CONFORMANT_CONFIG = 0x3051; @@ -200,6 +200,9 @@ namespace Avalonia.OpenGL.Egl // EXT_platform_device public const int EGL_PLATFORM_DEVICE_EXT = 0x313F; + // KHR_platform_x11 / EXT_platform_x11 + public const int EGL_PLATFORM_X11_EXT = 0x31D5; + //EXT_device_query public const int EGL_DEVICE_EXT = 0x322C; diff --git a/src/Avalonia.OpenGL/Egl/EglDisplay.cs b/src/Avalonia.OpenGL/Egl/EglDisplay.cs index 373aa0e964..1aefc20850 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplay.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplay.cs @@ -45,7 +45,7 @@ namespace Avalonia.OpenGL.Egl if(_display == IntPtr.Zero) throw new ArgumentException(); - _config = EglDisplayUtils.InitializeAndGetConfig(_egl, display, options.GlVersions); + _config = EglDisplayUtils.InitializeAndGetConfig(_egl, display, options.GlVersions, options.ProbeConfig); } public EglInterface EglInterface => _egl; @@ -84,7 +84,18 @@ namespace Avalonia.OpenGL.Egl } } - var ctx = _egl.CreateContext(_display, Config, share?.Context ?? IntPtr.Zero, _config.Attributes); + var previousApi = _egl.QueryApi(); + _egl.BindApi(_config.Api); + IntPtr ctx; + try + { + ctx = _egl.CreateContext(_display, Config, share?.Context ?? IntPtr.Zero, _config.Attributes); + } + finally + { + if (previousApi != EGL_NONE) + _egl.BindApi(previousApi); + } if (ctx == IntPtr.Zero) { var ex = OpenGlException.GetFormattedException("eglCreateContext", _egl); diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs index a2cbeb5b3d..2963890d32 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplayOptions.cs @@ -3,6 +3,14 @@ using System.Collections.Generic; namespace Avalonia.OpenGL.Egl; +/// +/// Given every EGL config matched by eglChooseConfig, returns the one that should be used, or null if none +/// are usable. This lets platforms filter out broken configs that can't be distinguished by their attributes +/// (e.g. nvidia exposes duplicate, partially broken configs) and impose their own preference order between +/// usable ones (e.g. preferring a transparent/32-bit X11 visual, which mesa lists after the opaque ones). +/// +public delegate IntPtr? EglConfigProbeCallback(EglInterface egl, IntPtr display, IntPtr[] configs); + public class EglDisplayOptions { public EglInterface? Egl { get; set; } @@ -12,6 +20,7 @@ public class EglDisplayOptions public Func? DeviceLostCheckCallback { get; set; } public Action? DisposeCallback { get; set; } public IEnumerable? GlVersions { get; set; } + public EglConfigProbeCallback? ProbeConfig { get; set; } } public class EglContextOptions diff --git a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs index 6ca7ab70c2..b214f6d85e 100644 --- a/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs +++ b/src/Avalonia.OpenGL/Egl/EglDisplayUtils.cs @@ -29,7 +29,28 @@ internal static class EglDisplayUtils return display; } - public static EglConfigInfo InitializeAndGetConfig(EglInterface egl, IntPtr display, IEnumerable? versions) + // Enumerates every config matching the attribute list and lets the probe callback pick one (or simply + // takes the first one if no probe is supplied). + private static IntPtr? ChooseConfigWithProbe(EglInterface egl, IntPtr display, int[] attribs, + EglConfigProbeCallback? probe) + { + if (!egl.ChooseConfigs(display, attribs, null, 0, out var numConfigs) || numConfigs == 0) + return null; + + var configs = new IntPtr[numConfigs]; + if (!egl.ChooseConfigs(display, attribs, configs, configs.Length, out numConfigs) || numConfigs == 0) + return null; + if (numConfigs != configs.Length) + Array.Resize(ref configs, numConfigs); + + if (probe == null) + return configs[0]; + + return probe(egl, display, configs); + } + + public static EglConfigInfo InitializeAndGetConfig(EglInterface egl, IntPtr display, + IEnumerable? versions, EglConfigProbeCallback? probeConfig = null) { if (!egl.Initialize(display, out _, out _)) throw OpenGlException.GetFormattedException("eglInitialize", egl); @@ -122,11 +143,8 @@ internal static class EglDisplayUtils EGL_DEPTH_SIZE, depthSize, EGL_NONE }; - if (!egl.ChooseConfig(display, attribs, out var config, 1, out int numConfigs)) + if (ChooseConfigWithProbe(egl, display, attribs, probeConfig) is not { } config) continue; - if (numConfigs == 0) - continue; - egl.GetConfigAttrib(display, config, EGL_SAMPLES, out var sampleCount); egl.GetConfigAttrib(display, config, EGL_STENCIL_SIZE, out var returnedStencilSize); diff --git a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs index 8ee06266d8..d3deb3d102 100644 --- a/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs +++ b/src/Avalonia.OpenGL/Egl/EglGlPlatformSurfaceBase.cs @@ -53,6 +53,21 @@ namespace Avalonia.OpenGL.Egl Context.GlInterface.BindFramebuffer(GlConsts.GL_FRAMEBUFFER, 0); + // Workaround for driver quirk https://github.com/NVIDIA/egl-wayland2/issues/46 + // This is NVIDIA-specific, but setting buffers to GL_BACK won't hurt for other drivers too + if (Context.Version.Type == GlProfileType.OpenGL) + { + var gl = Context.GlInterface; + gl.Viewport(0, 0, size.Width, size.Height); + if (gl.IsReadBufferAvailable) + gl.ReadBuffer(GlConsts.GL_BACK); + if (gl.IsWriteBufferAvailable) + gl.WriteBuffer(GlConsts.GL_BACK); + if(gl.IsDrawBufferAvailable) + gl.DrawBuffer(GlConsts.GL_BACK); + } + + success = true; return new Session(Context.Display, Context, surface, size, scaling, restoreContext, onFinish, isYFlipped, SkipWaits); } diff --git a/src/Avalonia.OpenGL/Egl/EglInterface.cs b/src/Avalonia.OpenGL/Egl/EglInterface.cs index a0210b544e..6b83731b49 100644 --- a/src/Avalonia.OpenGL/Egl/EglInterface.cs +++ b/src/Avalonia.OpenGL/Egl/EglInterface.cs @@ -75,7 +75,14 @@ namespace Avalonia.OpenGL.Egl [GetProcAddress("eglChooseConfig")] public partial bool ChooseConfig(IntPtr display, int[] attribs, out IntPtr surfaceConfig, int numConfigs, out int choosenConfig); - + + // Returns all configs matching the attribute list. Pass a null `configs` array to query the + // available config count first. Some drivers (notably nvidia) expose multiple indistinguishable + // configs where only a subset is actually usable, so callers need to enumerate and probe them. + [GetProcAddress("eglChooseConfig")] + public partial bool ChooseConfigs(IntPtr display, int[] attribs, + IntPtr[]? configs, int configSize, out int numConfigs); + [GetProcAddress("eglCreateContext")] public partial IntPtr CreateContext(IntPtr display, IntPtr config, IntPtr share, int[] attrs); diff --git a/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs b/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs index cf81999095..df0f80f1ce 100644 --- a/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs +++ b/src/Avalonia.OpenGL/Egl/EglPlatformGraphics.cs @@ -7,6 +7,7 @@ namespace Avalonia.OpenGL.Egl public sealed class EglPlatformGraphics : IPlatformGraphics { private readonly EglDisplay _display; + internal EglDisplay Display => _display; public bool UsesSharedContext => false; public IPlatformGraphicsContext CreateContext() => _display.CreateContext(null); public IPlatformGraphicsContext GetSharedContext() => throw new NotSupportedException(); diff --git a/src/Avalonia.OpenGL/GlConsts.cs b/src/Avalonia.OpenGL/GlConsts.cs index c1e1df0639..98b6e42da3 100644 --- a/src/Avalonia.OpenGL/GlConsts.cs +++ b/src/Avalonia.OpenGL/GlConsts.cs @@ -85,7 +85,7 @@ namespace Avalonia.OpenGL // public const int GL_CW = 0x0900; // public const int GL_CCW = 0x0901; // public const int GL_FRONT = 0x0404; -// public const int GL_BACK = 0x0405; + public const int GL_BACK = 0x0405; // public const int GL_POLYGON_MODE = 0x0B40; // public const int GL_POLYGON_SMOOTH = 0x0B41; // public const int GL_POLYGON_STIPPLE = 0x0B42; diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 63d265c41e..55eab4563b 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -388,6 +388,15 @@ namespace Avalonia.OpenGL [GlExtensionEntryPoint("glGenVertexArraysOES", "GL_OES_vertex_array_object")] public partial void GenVertexArrays(int n, int* rv); + [GetProcAddress("glReadBuffer", true)] + public partial void ReadBuffer(int buffer); + + [GetProcAddress("glDrawBuffer", true)] + public partial void DrawBuffer(int buffer); + + [GetProcAddress("glWriteBuffer", true)] + public partial void WriteBuffer(int buffer); + public int GenVertexArray() { int rv = 0; diff --git a/src/Avalonia.X11/X11EglHelper.cs b/src/Avalonia.X11/X11EglHelper.cs new file mode 100644 index 0000000000..2cb70a998a --- /dev/null +++ b/src/Avalonia.X11/X11EglHelper.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Logging; +using Avalonia.OpenGL.Egl; +using static Avalonia.OpenGL.Egl.EglConsts; +using static Avalonia.X11.XLib; + +namespace Avalonia.X11 +{ + internal static class X11EglHelper + { + /// + /// Resolves the that matches the EGL config's native visual id. + /// nvidia's driver requires the X11 window visual to match the config used to create the surface, + /// otherwise eglCreateWindowSurface fails. Returns null when the config doesn't advertise a visual id + /// (e.g. mesa, where any visual works). + /// + public static XVisualInfo? GetVisualInfo(X11Info x11, EglInterface egl, IntPtr display, IntPtr config) + { + if (!egl.GetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, out var visualId) || visualId == 0) + return null; + return XGetVisualInfoById(x11.DeferredDisplay, new IntPtr(visualId)); + } + + public static XVisualInfo? GetVisualInfo(X11Info x11, EglDisplay display) => + GetVisualInfo(x11, display.EglInterface, display.Handle, display.Config); + + /// + /// Picks the EGL config to use out of the ones matched by eglChooseConfig. Each candidate is verified by + /// creating a throwaway window with the config's native visual and attempting to create an EGL window + /// surface on it: nvidia exposes multiple identical-looking configs where only a subset actually works, + /// so the broken ones are discarded here. A 32-bit (transparent-capable) X11 visual is preferred, + /// mirroring the GLX backend which selects a 32-bit visual; mesa lists those configs after the opaque + /// ones. We resolve the (cheap) native visuals up-front and probe in preference order, so the expensive + /// window-surface creation is attempted as few times as possible. + /// + public static IntPtr? ChooseConfig(X11Info x11, EglInterface egl, IntPtr display, IntPtr[] configs) + { + var candidates = new List<(IntPtr config, XVisualInfo visual)>(configs.Length); + foreach (var config in configs) + { + if (GetVisualInfo(x11, egl, display, config) is { } visual) + candidates.Add((config, visual)); + else + Logger.TryGet(LogEventLevel.Verbose, "OpenGL") + ?.Log(null, "EGL config {Config} has no native visual id", config); + } + + // OrderByDescending is stable, so configs keep eglChooseConfig's relative order within each group. + foreach (var (config, visual) in candidates.OrderByDescending(c => c.visual.depth == 32)) + { + if (ProbeConfig(x11, egl, display, config, visual)) + return config; + } + + return null; + } + + private static bool ProbeConfig(X11Info x11, EglInterface egl, IntPtr display, IntPtr config, + XVisualInfo vi) + { + var colormap = XCreateColormap(x11.DeferredDisplay, x11.RootWindow, vi.visual, 0); + var attr = new XSetWindowAttributes + { + colormap = colormap, + border_pixel = IntPtr.Zero + }; + + var window = XCreateWindow(x11.DeferredDisplay, x11.RootWindow, 0, 0, 1, 1, 0, + (int)vi.depth, (int)CreateWindowArgs.InputOutput, vi.visual, + new UIntPtr((uint)(SetWindowValuemask.ColorMap | SetWindowValuemask.BorderPixel)), ref attr); + + if (window == IntPtr.Zero) + { + XFreeColormap(x11.DeferredDisplay, colormap); + return false; + } + + XFlush(x11.DeferredDisplay); + + var surface = egl.CreateWindowSurface(display, config, window, new[] { EGL_NONE, EGL_NONE }); + var success = surface != IntPtr.Zero; + if (success) + egl.DestroySurface(display, surface); + + XDestroyWindow(x11.DeferredDisplay, window); + XFreeColormap(x11.DeferredDisplay, colormap); + + Logger.TryGet(LogEventLevel.Verbose, "OpenGL")?.Log(null, + "EGL config {Config}: visualId={VisualId} depth={Depth} usable={Usable}", + config, vi.visualid, vi.depth, success); + + return success; + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 8b918aa27e..c043bf9bbf 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -227,13 +227,35 @@ namespace Avalonia.X11 if (renderingMode == X11RenderingMode.Egl) { - if (EglPlatformGraphics.TryCreate(()=>new EglDisplay(new EglDisplayCreationOptions() + if (EglPlatformGraphics.TryCreate(() => { - SupportsContextSharing = true, - SupportsMultipleContexts = true, - GlVersions = opts.GlProfiles, - Egl = new EglInterface() - })) is { } egl) + var egl = new EglInterface(); + var options = new EglDisplayCreationOptions + { + SupportsContextSharing = true, + SupportsMultipleContexts = true, + GlVersions = opts.GlProfiles, + Egl = egl, + // nvidia exposes multiple indistinguishable configs of which only some work, + // so we probe candidates by creating a throwaway window surface and pick a usable one. + ProbeConfig = (probeEgl, display, configs) => + X11EglHelper.ChooseConfig(info, probeEgl, display, configs) + }; + + // nvidia requires the display to be created through the X11 platform extension, + // otherwise EGL_NATIVE_VISUAL_ID doesn't match the actual window visual. + var clientExtensions = egl.QueryString(IntPtr.Zero, EglConsts.EGL_EXTENSIONS); + if (egl.IsGetPlatformDisplayExtAvailable + && clientExtensions != null + && (clientExtensions.Contains("EGL_KHR_platform_x11") + || clientExtensions.Contains("EGL_EXT_platform_x11"))) + { + options.PlatformType = EglConsts.EGL_PLATFORM_X11_EXT; + options.PlatformDisplay = info.DeferredDisplay; + } + + return new EglDisplay(options); + }) is { } egl) { return egl; } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 91fbfb93d2..67b97979cc 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -125,8 +125,9 @@ namespace Avalonia.X11 // OpenGL seems to be do weird things to it's current window which breaks resize sometimes _useRenderWindow = glfeature != null; - + var glx = glfeature as GlxPlatformGraphics; + var egl = glfeature as EglPlatformGraphics; if (glx != null) { visualInfo = *glx.Display.VisualInfo; @@ -134,11 +135,13 @@ namespace Avalonia.X11 // the target sufrace currently is _useCompositorDrivenRenderWindowResize = true; } + else if (egl != null) + { + visualInfo = X11EglHelper.GetVisualInfo(_x11, egl.Display); + } else if (glfeature == null) visualInfo = _x11.TransparentVisualInfo; - var egl = glfeature as EglPlatformGraphics; - var visual = IntPtr.Zero; var depth = 24; if (visualInfo != null) @@ -177,11 +180,17 @@ namespace Avalonia.X11 if (_useRenderWindow) { + var renderValueMask = SetWindowValuemask.BorderPixel | SetWindowValuemask.BitGravity | + SetWindowValuemask.WinGravity | SetWindowValuemask.BackingStore; + // A window with a non-default visual must be created with a matching colormap, otherwise X11 + // raises BadMatch. This is required by nvidia when a custom visual is selected for the EGL config. + if (visualInfo != null) + renderValueMask |= SetWindowValuemask.ColorMap; + _renderHandle = XCreateWindow(_x11.Display, _handle, 0, 0, defaultWidth, defaultHeight, 0, depth, (int)CreateWindowArgs.InputOutput, visual, - new UIntPtr((uint)(SetWindowValuemask.BorderPixel | SetWindowValuemask.BitGravity | - SetWindowValuemask.WinGravity | SetWindowValuemask.BackingStore)), ref attr); + new UIntPtr((uint)renderValueMask), ref attr); } else { @@ -281,6 +290,11 @@ namespace Avalonia.X11 }); platform.X11Screens.Changed += OnScreensChanged; + + // The render surface (EGL/GLX) is created on the deferred display connection, which is a separate + // X11 connection from the one the windows were created on. Force a round-trip so the server has + // actually created the windows before anything on the other connection tries to use them. + XSync(_x11.Display, false); } private class SurfaceInfo : EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo diff --git a/src/Avalonia.X11/XLib.cs b/src/Avalonia.X11/XLib.cs index 595e996733..0b663d7a1b 100644 --- a/src/Avalonia.X11/XLib.cs +++ b/src/Avalonia.X11/XLib.cs @@ -459,7 +459,27 @@ namespace Avalonia.X11 [DllImport(libX11)] public static extern IntPtr XCreateColormap(IntPtr display, IntPtr window, IntPtr visual, int create); - + + [DllImport(libX11)] + public static extern int XFreeColormap(IntPtr display, IntPtr colormap); + + public const long VisualIDMask = 0x1; + + [DllImport(libX11)] + public static extern IntPtr XGetVisualInfo(IntPtr display, IntPtr vinfo_mask, ref XVisualInfo vinfo_template, + out int nitems); + + public static unsafe XVisualInfo? XGetVisualInfoById(IntPtr display, IntPtr visualId) + { + var template = new XVisualInfo { visualid = visualId }; + var ptr = XGetVisualInfo(display, new IntPtr(VisualIDMask), ref template, out var count); + if (ptr == IntPtr.Zero) + return null; + XVisualInfo? rv = count > 0 ? *(XVisualInfo*)ptr : null; + XFree(ptr); + return rv; + } + public enum XLookupStatus : uint { XBufferOverflow = 0xffffffffu,