source/SkiaSharp.Views/SkiaSharp.Views.WinUI/AngleSwapChainPanel.cs (236 lines of code) (raw):
using System;
using System.Threading.Tasks;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Data;
using SkiaSharp.Views.GlesInterop;
using Windows.ApplicationModel;
using Windows.Foundation;
using Windows.System.Threading;
using Windows.UI.Core;
#if WINDOWS
namespace SkiaSharp.Views.Windows
#else
namespace SkiaSharp.Views.UWP
#endif
{
public class AngleSwapChainPanel : SwapChainPanel
{
private static readonly DependencyProperty ProxyVisibilityProperty =
DependencyProperty.Register(
"ProxyVisibility",
typeof(Visibility),
typeof(AngleSwapChainPanel),
new PropertyMetadata(Visibility.Visible, OnVisibilityChanged));
private static readonly bool designMode = DesignMode.DesignModeEnabled;
private readonly object locker = new object();
private bool isVisible = true;
private bool isLoaded = false;
private GlesContext glesContext;
private IAsyncAction renderLoopWorker;
private IAsyncAction renderOnceWorker;
private bool enableRenderLoop;
private double lastCompositionScaleX = 0.0;
private double lastCompositionScaleY = 0.0;
private bool pendingSizeChange = false;
public AngleSwapChainPanel()
{
lastCompositionScaleX = CompositionScaleX;
lastCompositionScaleY = CompositionScaleY;
glesContext = null;
renderLoopWorker = null;
renderOnceWorker = null;
DrawInBackground = false;
EnableRenderLoop = false;
ContentsScale = CompositionScaleX;
Loaded += OnLoaded;
Unloaded += OnUnloaded;
CompositionScaleChanged += OnCompositionChanged;
SizeChanged += OnSizeChanged;
var binding = new Binding
{
Path = new PropertyPath(nameof(Visibility)),
Source = this
};
SetBinding(ProxyVisibilityProperty, binding);
}
public bool DrawInBackground { get; set; }
public double ContentsScale { get; private set; }
public bool EnableRenderLoop
{
get => enableRenderLoop;
set
{
if (enableRenderLoop != value)
{
enableRenderLoop = value;
UpdateRenderLoop(value);
}
}
}
public void Invalidate()
{
if (!isLoaded || EnableRenderLoop)
return;
if (DrawInBackground)
{
lock (locker)
{
// if we haven't fired a render thread, start one
if (renderOnceWorker == null)
{
renderOnceWorker = ThreadPool.RunAsync(RenderOnce);
}
}
}
else
{
// draw on this thread, blocking
RenderFrame();
}
}
protected virtual void OnRenderFrame(Rect rect)
{
}
protected virtual void OnDestroyingContext()
{
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
glesContext = new GlesContext();
isLoaded = true;
ContentsScale = CompositionScaleX;
EnsureRenderSurface();
UpdateRenderLoop(EnableRenderLoop);
Invalidate();
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
OnDestroyingContext();
CompositionScaleChanged -= OnCompositionChanged;
SizeChanged -= OnSizeChanged;
UpdateRenderLoop(false);
DestroyRenderSurface();
isLoaded = false;
glesContext?.Dispose();
glesContext = null;
}
private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is AngleSwapChainPanel panel && e.NewValue is Visibility visibility)
{
panel.isVisible = visibility == Visibility.Visible;
panel.UpdateRenderLoop(panel.isVisible && panel.EnableRenderLoop);
panel.Invalidate();
}
}
private void OnCompositionChanged(SwapChainPanel sender, object args)
{
if (lastCompositionScaleX == CompositionScaleX &&
lastCompositionScaleY == CompositionScaleY)
{
return;
}
lastCompositionScaleX = CompositionScaleX;
lastCompositionScaleY = CompositionScaleY;
pendingSizeChange = true;
ContentsScale = CompositionScaleX;
DestroyRenderSurface();
EnsureRenderSurface();
Invalidate();
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
pendingSizeChange = true;
EnsureRenderSurface();
Invalidate();
}
private void EnsureRenderSurface()
{
if (isLoaded && glesContext?.HasSurface != true && ActualWidth > 0 && ActualHeight > 0)
{
// detach and re-attach the size events as we need to go after the event added by ANGLE
// otherwise our size will still be the old size
SizeChanged -= OnSizeChanged;
CompositionScaleChanged -= OnCompositionChanged;
glesContext.CreateSurface(this, null, CompositionScaleX);
SizeChanged += OnSizeChanged;
CompositionScaleChanged += OnCompositionChanged;
}
}
private void DestroyRenderSurface()
{
glesContext?.DestroySurface();
}
private void RenderFrame()
{
if (designMode || !isLoaded || !isVisible || glesContext?.HasSurface != true)
return;
glesContext.MakeCurrent();
if (pendingSizeChange)
{
pendingSizeChange = false;
if (!EnableRenderLoop)
glesContext.SwapBuffers();
}
glesContext.GetSurfaceDimensions(out var panelWidth, out var panelHeight);
glesContext.SetViewportSize(panelWidth, panelHeight);
OnRenderFrame(new Rect(0, 0, panelWidth, panelHeight));
if (!glesContext.SwapBuffers())
{
// The call to eglSwapBuffers might not be successful (i.e. due to Device Lost)
// If the call fails, then we must reinitialize EGL and the GL resources.
}
}
private void UpdateRenderLoop(bool start)
{
if (!isLoaded)
return;
lock (locker)
{
if (start)
{
// if the render loop is not running, start it
if (renderLoopWorker?.Status != AsyncStatus.Started)
{
renderLoopWorker = ThreadPool.RunAsync(RenderLoop);
}
}
else
{
// stop the current render loop
renderLoopWorker?.Cancel();
renderLoopWorker = null;
}
}
}
private void RenderOnce(IAsyncAction action)
{
if (DrawInBackground)
{
// run on this background thread
RenderFrame();
}
else
{
// run in the main thread, block this one
Dispatcher.RunAsync(CoreDispatcherPriority.Normal, RenderFrame).AsTask().Wait();
}
lock (locker)
{
// we are finished, so null out
renderOnceWorker = null;
}
}
private void RenderLoop(IAsyncAction action)
{
while (action.Status == AsyncStatus.Started)
{
if (DrawInBackground)
{
// run on this background thread
RenderFrame();
}
else
{
// run in the main thread, block this one
var tcs = new TaskCompletionSource();
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, () =>
{
RenderFrame();
tcs.SetResult();
});
tcs.Task.Wait();
}
}
}
}
}