Skip to content

Commit

Permalink
feat: Improve automatic OpenGL/ES detection
Browse files Browse the repository at this point in the history
Use a temporary GLValidationSurface to ensure that the Skia GRGl content can be completely validated with an active OpenGL context.
  • Loading branch information
jeromelaban committed Sep 4, 2022
1 parent 3ebb40b commit f614a88
Show file tree
Hide file tree
Showing 4 changed files with 282 additions and 88 deletions.
90 changes: 90 additions & 0 deletions src/Uno.UI.Runtime.Skia.Gtk/GLValidationSurface.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#nullable enable

using System;
using System.Threading.Tasks;
using Gtk;
using Uno.Foundation.Logging;

namespace Uno.UI.Runtime.Skia
{
/// <summary>
/// Validation surface for OpenGL rendering, as CreateGRGLContext
/// needs to be invoked with an active OpenGL context, and that Skia
/// does additional validation that cannot be extracted at an earlier stage.
/// </summary>
internal class GLValidationSurface : GLArea
{
private TaskCompletionSource<RenderSurfaceType> _result = new();

public GLValidationSurface()
{
HasDepthBuffer = false;
HasStencilBuffer = false;
AutoRender = true;

Render += GLValidationSurface_Render;
}

private void GLValidationSurface_Render(object o, RenderArgs args)
{
if (typeof(GLValidationSurface).Log().IsEnabled(LogLevel.Debug))
{
typeof(GLValidationSurface).Log().Debug($"GLValidationSurface: UseEs={UseEs}");
}

args.Context.MakeCurrent();

ValidateOpenGLSupport();
}

private void ValidateOpenGLSupport()
{
try
{
if (OpenGLESRenderSurface.IsSupported)
{
using var ctx = OpenGLESRenderSurface.CreateGRGLContext();

if (ctx != null)
{
_result.TrySetResult(RenderSurfaceType.OpenGLES);
return;
}
}
}
catch (Exception e)
{
if (typeof(GLValidationSurface).Log().IsEnabled(LogLevel.Debug))
{
typeof(GLValidationSurface).Log().Debug($"OpenGL ES cannot be used ({e.Message})");
}
}

try
{
if (OpenGLRenderSurface.IsSupported)
{
using var ctx = OpenGLRenderSurface.CreateGRGLContext();

if (ctx != null)
{
_result.TrySetResult(RenderSurfaceType.OpenGL);
return;
}
}
}
catch (Exception e)
{
if (typeof(GLValidationSurface).Log().IsEnabled(LogLevel.Debug))
{
typeof(GLValidationSurface).Log().Debug($"OpenGL cannot be used ({e.Message})");
}
}

_result.TrySetResult(RenderSurfaceType.Software);
}

internal Task<RenderSurfaceType> GetSurfaceTypeAsync()
=> _result.Task;
}
}
194 changes: 143 additions & 51 deletions src/Uno.UI.Runtime.Skia.Gtk/GtkHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
using Uno.UI.Xaml.Core;
using System.ComponentModel;
using Uno.Disposables;
using System.Collections.Generic;

namespace Uno.UI.Runtime.Skia
{
Expand All @@ -49,6 +50,9 @@ public class GtkHost : ISkiaHost
private GtkDisplayInformationExtension _displayInformationExtension;
private CompositeDisposable _registrations = new();

private record PendingWindowStateChangedInfo(Gdk.WindowState newState, Gdk.WindowState changedMask);
private List<PendingWindowStateChangedInfo> _pendingWindowStateChanged = new();

public static Gtk.Window Window => _window;
internal static UnoEventBox EventBox => _eventBox;

Expand Down Expand Up @@ -120,38 +124,101 @@ public void Run()

_window.DeleteEvent += WindowClosing;

void Dispatch(System.Action d)
Windows.UI.Core.CoreDispatcher.DispatchOverride = DispatchNative;
Windows.UI.Core.CoreDispatcher.HasThreadAccessOverride = () => _isDispatcherThread;

_window.WindowStateEvent += OnWindowStateChanged;
_window.ShowAll();

SetupRenderSurface();

Gtk.Application.Run();
}

void DispatchNative(System.Action d)
{
if (Gtk.Application.EventsPending())
{
if (Gtk.Application.EventsPending())
Gtk.Application.RunIteration(false);
}

GLib.Idle.Add(delegate
{
if (this.Log().IsEnabled(LogLevel.Trace))
{
Gtk.Application.RunIteration(false);
this.Log().Trace($"Iteration");
}

GLib.Idle.Add(delegate
try
{
if (this.Log().IsEnabled(LogLevel.Trace))
{
this.Log().Trace($"Iteration");
}
d();
}
catch (Exception e)
{
Console.WriteLine(e);
}

return false;
});
}

private void SetupRenderSurface()
{
TryReadRenderSurfaceTypeEnvironment();

if (RenderSurfaceType == null)
{
// Create a temporary surface to automatically detect
// the OpenGL environment that can be used on the system.
GLValidationSurface validationSurface = new();

_window.Add(validationSurface);
_window.ShowAll();

DispatchNative(ValidatedSurface);

async void ValidatedSurface()
{
try
{
d();
if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().Debug($"Auto-detecting surface type");
}

// Wait for a realization of the GLValidationSurface
RenderSurfaceType = await validationSurface.GetSurfaceTypeAsync();

// Continue on the GTK main thread
DispatchNative(() =>
{
if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().Debug($"Auto-detected {RenderSurfaceType} rendering");
}

_window.Remove(validationSurface);

FinalizeStartup();
});
}
catch (Exception e)
{
Console.WriteLine(e);
if (this.Log().IsEnabled(LogLevel.Error))
{
this.Log().Error($"Auto-detected failed", e);
}
}

return false;
});
}
}
else
{
FinalizeStartup();
}
}

Windows.UI.Core.CoreDispatcher.DispatchOverride = Dispatch;
Windows.UI.Core.CoreDispatcher.HasThreadAccessOverride = () => _isDispatcherThread;

_window.WindowStateEvent += OnWindowStateChanged;

private void FinalizeStartup()
{
var overlay = new Overlay();

_eventBox = new UnoEventBox();
Expand All @@ -164,6 +231,10 @@ void Dispatch(System.Action d)
_eventBox.Add(overlay);
_window.Add(_eventBox);

// Show the whole tree again, since we may have
// swapped the content with the GLValidationSurface.
_window.ShowAll();

if (this.Log().IsEnabled(LogLevel.Information))
{
this.Log().Info($"Using {RenderSurfaceType} rendering");
Expand All @@ -182,7 +253,7 @@ void Dispatch(System.Action d)
/* avoids double invokes at window level */
_area.AddEvents((int)GtkCoreWindowExtension.RequestedEvents);

_window.ShowAll();
ReplayPendingWindowStateChanges();

void CreateApp(ApplicationInitializationCallbackParams _)
{
Expand All @@ -191,18 +262,28 @@ void CreateApp(ApplicationInitializationCallbackParams _)
}

CoreServices.Instance.ContentRootCoordinator.CoreWindowContentRootSet += OnCoreWindowContentRootSet;

WUX.Application.StartWithArguments(CreateApp);

_window.Title = Windows.ApplicationModel.Package.Current.DisplayName;

RegisterForBackgroundColor();

UpdateWindowPropertiesFromPackage();

Gtk.Application.Run();
UpdateWindowPropertiesFromPackage();
}

private void TryReadRenderSurfaceTypeEnvironment()
{
if (Enum.TryParse(Environment.GetEnvironmentVariable("UNO_RENDER_SURFACE_TYPE"), out RenderSurfaceType surfaceType))
{
if (this.Log().IsEnabled(LogLevel.Debug))
{
this.Log().Debug($"Overriding RnderSurfaceType using command line with {surfaceType}");
}

RenderSurfaceType = surfaceType;
}
}
private void RegisterForBackgroundColor()
{
if (_area is IRenderSurface renderSurface)
Expand Down Expand Up @@ -268,54 +349,65 @@ private void WindowClosing(object sender, DeleteEventArgs args)
}

private IRenderSurface BuildRenderSurfaceType()
=> RenderSurfaceType switch
{
Skia.RenderSurfaceType.OpenGLES => new OpenGLESRenderSurface(),
Skia.RenderSurfaceType.OpenGL => new OpenGLRenderSurface(),
Skia.RenderSurfaceType.Software => new SoftwareRenderSurface(),
_ => throw new InvalidOperationException($"Unsupported RenderSurfaceType {RenderSurfaceType}")
};

private void OnWindowStateChanged(object o, WindowStateEventArgs args)
{
if (RenderSurfaceType == null)
var newState = args.Event.NewWindowState;
var changedMask = args.Event.ChangedMask;

if (this.Log().IsEnabled(LogLevel.Debug))
{
if (OpenGLESRenderSurface.IsSupported)
{
RenderSurfaceType = Skia.RenderSurfaceType.OpenGLES;
}
else if (OpenGLRenderSurface.IsSupported)
{
RenderSurfaceType = Skia.RenderSurfaceType.OpenGL;
}
else
{
RenderSurfaceType = Skia.RenderSurfaceType.Software;
}
this.Log().Debug($"OnWindowStateChanged: {newState}/{changedMask}");
}

if (this.Log().IsEnabled(LogLevel.Information))
if (_area != null)
{
ProcessWindowStateChanged(newState, changedMask);
}
else
{
this.Log().LogInfo($"Using {RenderSurfaceType} render surface");
// Store state changes to replay once the application has been
// initalized completely (initialization can be delayed if the render
// surface is automatically detected).
_pendingWindowStateChanged?.Add(new(newState, changedMask));
}
}

return RenderSurfaceType switch
private void ReplayPendingWindowStateChanges()
{
if(_pendingWindowStateChanged is not null)
{
Skia.RenderSurfaceType.OpenGLES => new OpenGLESRenderSurface(),
Skia.RenderSurfaceType.OpenGL => new OpenGLRenderSurface(),
Skia.RenderSurfaceType.Software => new SoftwareRenderSurface(),
_ => throw new InvalidOperationException($"Unsupported RenderSurfaceType {RenderSurfaceType}")
};
foreach(var state in _pendingWindowStateChanged)
{
ProcessWindowStateChanged(state.newState, state.changedMask);
}

_pendingWindowStateChanged = null;
}
}

private void OnWindowStateChanged(object o, WindowStateEventArgs args)
private static void ProcessWindowStateChanged(Gdk.WindowState newState, Gdk.WindowState changedMask)
{
var winUIApplication = WUX.Application.Current;
var winUIWindow = WUX.Window.Current;
var newState = args.Event.NewWindowState;
var changedState = args.Event.ChangedMask;

var isVisible =
!(newState.HasFlag(Gdk.WindowState.Withdrawn) ||
newState.HasFlag(Gdk.WindowState.Iconified));

var isVisibleChanged =
changedState.HasFlag(Gdk.WindowState.Withdrawn) ||
changedState.HasFlag(Gdk.WindowState.Iconified);
changedMask.HasFlag(Gdk.WindowState.Withdrawn) ||
changedMask.HasFlag(Gdk.WindowState.Iconified);

var focused = newState.HasFlag(Gdk.WindowState.Focused);
var focusChanged = changedState.HasFlag(Gdk.WindowState.Focused);
var focusChanged = changedMask.HasFlag(Gdk.WindowState.Focused);

if (!focused && focusChanged)
{
Expand All @@ -326,7 +418,7 @@ private void OnWindowStateChanged(object o, WindowStateEventArgs args)
{
if (isVisible)
{
winUIApplication?.RaiseLeavingBackground(() => winUIWindow?.OnVisibilityChanged(true));
winUIApplication?.RaiseLeavingBackground(() => winUIWindow?.OnVisibilityChanged(true));
}
else
{
Expand Down
Loading

0 comments on commit f614a88

Please sign in to comment.