From a90164a9e6c1dc91091d55982270a42f4d46d338 Mon Sep 17 00:00:00 2001 From: Dedmen Miller Date: Tue, 23 Mar 2021 21:02:33 +0100 Subject: [PATCH] Add OpenGL rendering --- src/DesktopClient/Misc/MapControl.cs | 68 ++++++- src/DesktopClient/Misc/SKElement.cs | 126 +++++++++++++ src/DesktopClient/Misc/SKGLWpfControl.cs | 226 +++++++++++++++++++++++ src/DesktopClient/TacControl.csproj | 15 +- 4 files changed, 427 insertions(+), 8 deletions(-) create mode 100644 src/DesktopClient/Misc/SKElement.cs create mode 100644 src/DesktopClient/Misc/SKGLWpfControl.cs diff --git a/src/DesktopClient/Misc/MapControl.cs b/src/DesktopClient/Misc/MapControl.cs index b8924f5..e958e9e 100644 --- a/src/DesktopClient/Misc/MapControl.cs +++ b/src/DesktopClient/Misc/MapControl.cs @@ -36,6 +36,23 @@ public enum RenderMode Wpf } + + public interface ISkiaCanvas + { + + [Category("Appearance")] + event EventHandler PaintSurface; + + [Category("Appearance")] + event EventHandler PaintSurfaceGL; + + Visibility Visibility{ get; set; } + + void InvalidateVisual(); + + + } + public partial class MapControl : Grid, IMapControl { //https://github.com/Mapsui/Mapsui/blob/af2bf64d3f45c0a3a7b91d2d58cc2a4fff3d13d3/Mapsui.UI.Shared/MapControl.cs @@ -521,14 +538,32 @@ public void Clear() /// public event EventHandler Fling; + static private bool GLRunning = false; + public MapControl() { Children.Add(WpfCanvas); - Children.Add(SkiaCanvas); - Children.Add(_selectRectangle); + if (!GLRunning) + { + SkiaCanvas = CreateSkiaGLRenderElement(); CreateSkiaRenderElement(); + GLRunning = true; + + Children.Add(SkiaCanvas as SKGLWpfControl); + } + else + { + SkiaCanvas = CreateSkiaRenderElement(); + + Children.Add(SkiaCanvas as SKElement); + } + + SkiaCanvas.PaintSurfaceGL += SKGLElementOnPaintSurface; SkiaCanvas.PaintSurface += SKElementOnPaintSurface; - + + + Children.Add(_selectRectangle); + Map = new Map(); Loaded += MapControlLoaded; @@ -578,7 +613,7 @@ private static Rectangle CreateSelectRectangle() public Canvas WpfCanvas { get; } = CreateWpfRenderCanvas(); - private SKElement SkiaCanvas { get; } = CreateSkiaRenderElement(); + private ISkiaCanvas SkiaCanvas { get; } public RenderMode RenderMode { @@ -613,6 +648,13 @@ private static Canvas CreateWpfRenderCanvas() }; } + private static SKGLWpfControl CreateSkiaGLRenderElement() + { + + return new SKGLWpfControl(); + } + + private static SKElement CreateSkiaRenderElement() { return new SKElement @@ -622,6 +664,8 @@ private static SKElement CreateSkiaRenderElement() }; } + + public event EventHandler FeatureInfo; // todo: Remove and add sample for alternative public void RefreshGraphics() @@ -957,7 +1001,21 @@ private void OnManipulationCompleted(object sender, ManipulationCompletedEventAr Refresh(); } - private void SKElementOnPaintSurface(object sender, SKPaintSurfaceEventArgs args) + private void SKElementOnPaintSurface(object sender, SKPaintSurfaceEventArgs args) // + { + if (Renderer == null) return; + if (_map == null) return; + if (PixelDensity <= 0) return; + + args.Surface.Canvas.Scale(PixelDensity, PixelDensity); + + Navigator.UpdateAnimations(); + Renderer.Render(args.Surface.Canvas, new Viewport(Viewport), Map.Layers, Map.Widgets, Map.BackColor); + } + + + + private void SKGLElementOnPaintSurface(object sender, SKPaintGLSurfaceEventArgs args) // SKPaintSurfaceEventArgs { if (Renderer == null) return; if (_map == null) return; diff --git a/src/DesktopClient/Misc/SKElement.cs b/src/DesktopClient/Misc/SKElement.cs new file mode 100644 index 0000000..f61cdc5 --- /dev/null +++ b/src/DesktopClient/Misc/SKElement.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; +using System.Windows.Media.Imaging; +using SkiaSharp; +using SkiaSharp.Views.Desktop; + +namespace TacControl.Misc +{ + [DefaultEvent("PaintSurface")] + [DefaultProperty("Name")] + public class SKElement : FrameworkElement, ISkiaCanvas + { + private readonly bool designMode; + + private WriteableBitmap bitmap; + private bool ignorePixelScaling; + + public SKElement() + { + designMode = DesignerProperties.GetIsInDesignMode(this); + } + + [Bindable(false)] + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [EditorBrowsable(EditorBrowsableState.Never)] + public SKSize CanvasSize => bitmap == null ? SKSize.Empty : new SKSize(bitmap.PixelWidth, bitmap.PixelHeight); + + public bool IgnorePixelScaling + { + get { return ignorePixelScaling; } + set + { + ignorePixelScaling = value; + InvalidateVisual(); + } + } + + // fake, for interface + [Category("Appearance")] + public event EventHandler PaintSurfaceGL; + + [Category("Appearance")] + public event EventHandler PaintSurface; + + protected override void OnRender(DrawingContext drawingContext) + { + base.OnRender(drawingContext); + + if (designMode) + return; + + if (Visibility != Visibility.Visible || PresentationSource.FromVisual(this) == null) + return; + + var size = CreateSize(out var scaleX, out var scaleY); + if (size.Width <= 0 || size.Height <= 0) + return; + + var info = new SKImageInfo(size.Width, size.Height, SKImageInfo.PlatformColorType, SKAlphaType.Premul); + + // reset the bitmap if the size has changed + if (bitmap == null || info.Width != bitmap.PixelWidth || info.Height != bitmap.PixelHeight) + { + bitmap = new WriteableBitmap(info.Width, size.Height, 96 * scaleX, 96 * scaleY, PixelFormats.Pbgra32, null); + } + + // draw on the bitmap + bitmap.Lock(); + using (var surface = SKSurface.Create(info, bitmap.BackBuffer, bitmap.BackBufferStride)) + { + OnPaintSurface(new SKPaintSurfaceEventArgs(surface, info)); + } + + // draw the bitmap to the screen + bitmap.AddDirtyRect(new Int32Rect(0, 0, info.Width, size.Height)); + bitmap.Unlock(); + drawingContext.DrawImage(bitmap, new Rect(0, 0, ActualWidth, ActualHeight)); + } + + protected virtual void OnPaintSurface(SKPaintSurfaceEventArgs e) + { + // invoke the event + PaintSurface?.Invoke(this, e); + } + + protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) + { + base.OnRenderSizeChanged(sizeInfo); + + InvalidateVisual(); + } + + private SKSizeI CreateSize(out double scaleX, out double scaleY) + { + scaleX = 1.0; + scaleY = 1.0; + + var w = ActualWidth; + var h = ActualHeight; + + if (!IsPositive(w) || !IsPositive(h)) + return SKSizeI.Empty; + + if (IgnorePixelScaling) + return new SKSizeI((int)w, (int)h); + + var m = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice; + scaleX = m.M11; + scaleY = m.M22; + return new SKSizeI((int)(w * scaleX), (int)(h * scaleY)); + + bool IsPositive(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value) && value > 0; + } + } + } +} diff --git a/src/DesktopClient/Misc/SKGLWpfControl.cs b/src/DesktopClient/Misc/SKGLWpfControl.cs new file mode 100644 index 0000000..ec7fb61 --- /dev/null +++ b/src/DesktopClient/Misc/SKGLWpfControl.cs @@ -0,0 +1,226 @@ +using System; +using System.ComponentModel; +using System.Windows; +using OpenTK.Graphics.OpenGL; +using OpenTK.Wpf; +using SkiaSharp; +using SkiaSharp.Views.Desktop; + +namespace TacControl.Misc +{ + // https://github.com/mono/SkiaSharp/issues/745 + // https://github.com/enissimsek + + + public struct SizeWithDpi + { + public static SizeWithDpi Empty = new SizeWithDpi(0, 0); + + public int Width; + public int Height; + public double DpiX; + public double DpiY; + + public SizeWithDpi(int width, int height, double dpiX = 96.0, double dpiY = 96.0) + { + Width = width; + Height = height; + DpiX = dpiX; + DpiY = dpiY; + } + + public override bool Equals(object obj) + { + return obj is SizeWithDpi dpi && + Width == dpi.Width && + Height == dpi.Height && + DpiX == dpi.DpiX && + DpiY == dpi.DpiY; + } + + public bool Equals(SizeWithDpi dpi) + { + return Width == dpi.Width && + Height == dpi.Height && + DpiX == dpi.DpiX && + DpiY == dpi.DpiY; + } + } + + + [DefaultEvent("PaintSurface")] + [DefaultProperty("Name")] + public class SKGLWpfControl : GLWpfControl, ISkiaCanvas + { + private const SKColorType colorType = SKColorType.Rgba8888; + private const GRSurfaceOrigin surfaceOrigin = GRSurfaceOrigin.BottomLeft; + + private bool designMode; + private bool disposed; + + private GRContext grContext; + private GRGlFramebufferInfo glInfo; + private GRBackendRenderTarget renderTarget; + private SKSurface surface; + private SKCanvas canvas; + + private bool ignorePixelScaling; + + private SKSizeI lastSize; + + public SKGLWpfControl() + { + Initialize(); + } + + private void Initialize() + { + designMode = DesignerProperties.GetIsInDesignMode(this); + + var mainSettings = new GLWpfControlSettings { MajorVersion = 2, MinorVersion = 1 }; + + this.Render += TkRender; + + this.Loaded += (s, e) => + { + Window.GetWindow(this).Closing += (s1, e1) => + { + this.Dispose(); + }; + }; + + Start(mainSettings); + } + + [Bindable(false)] + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [EditorBrowsable(EditorBrowsableState.Never)] + public SKSize CanvasSize => lastSize; + + [Bindable(false)] + [Browsable(false)] + [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [EditorBrowsable(EditorBrowsableState.Never)] + public GRContext GRContext => grContext; + + [Category("Appearance")] + public event EventHandler PaintSurfaceGL; + + // fake, for interface + [Category("Appearance")] + public event EventHandler PaintSurface; + + public bool IgnorePixelScaling + { + get { return ignorePixelScaling; } + set + { + ignorePixelScaling = value; + InvalidateVisual(); + } + } + + protected void TkRender(TimeSpan delta) + { + if (designMode || disposed) + return; + + if (this.Visibility != Visibility.Visible) + return; + + var size = CreateSize(); + if (size.Width <= 0 || size.Height <= 0) + return; + + // create the contexts if not done already + if (grContext == null) + { + var glInterface = GRGlInterface.Create(); + grContext = GRContext.CreateGl(glInterface); + } + + // get the new surface size + var newSize = new SKSizeI(size.Width, size.Height); + + // manage the drawing surface + if (renderTarget == null || lastSize != newSize || !renderTarget.IsValid) + { + // create or update the dimensions + lastSize = newSize; + + GL.GetInteger(GetPName.FramebufferBinding, out var framebuffer); + GL.GetInteger(GetPName.StencilBits, out var stencil); + GL.GetInteger(GetPName.Samples, out var samples); + var maxSamples = grContext.GetMaxSurfaceSampleCount(colorType); + if (samples > maxSamples) + samples = maxSamples; + glInfo = new GRGlFramebufferInfo((uint)framebuffer, colorType.ToGlSizedFormat()); + + // destroy the old surface + surface?.Dispose(); + surface = null; + canvas = null; + + // re-create the render target + renderTarget?.Dispose(); + renderTarget = new GRBackendRenderTarget(newSize.Width, newSize.Height, samples, stencil, glInfo); + } + + // create the surface + if (surface == null) + { + surface = SKSurface.Create(grContext, renderTarget, surfaceOrigin, colorType); + canvas = surface.Canvas; + } + + using (new SKAutoCanvasRestore(canvas, true)) + { + // start drawing + OnPaintSurface(new SKPaintGLSurfaceEventArgs(surface, renderTarget, surfaceOrigin, colorType, glInfo)); + } + + // update the control + canvas.Flush(); + } + + private SizeWithDpi CreateSize() + { + var w = ActualWidth; + var h = ActualHeight; + + if (!IsPositive(w) || !IsPositive(h)) + return SizeWithDpi.Empty; + + if (IgnorePixelScaling) + return new SizeWithDpi((int)w, (int)h); + + var m = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice; + return new SizeWithDpi((int)(w * m.M11), (int)(h * m.M22), 96.0 * m.M11, 96.0 * m.M22); + + bool IsPositive(double value) + { + return !double.IsNaN(value) && !double.IsInfinity(value) && value > 0; + } + } + + protected virtual void OnPaintSurface(SKPaintGLSurfaceEventArgs e) + { + PaintSurfaceGL?.Invoke(this, e); + } + + protected void Dispose() + { + // clean up + canvas = null; + surface?.Dispose(); + surface = null; + renderTarget?.Dispose(); + renderTarget = null; + grContext?.Dispose(); + grContext = null; + + disposed = true; + } + } +} diff --git a/src/DesktopClient/TacControl.csproj b/src/DesktopClient/TacControl.csproj index 39fa355..0613a19 100644 --- a/src/DesktopClient/TacControl.csproj +++ b/src/DesktopClient/TacControl.csproj @@ -51,12 +51,13 @@ bin\x64\Release\ TRACE true - pdbonly + full x64 7.3 prompt MinimumRecommendedRules.ruleset true + true TacControl.MainSentry @@ -131,6 +132,8 @@ + + RadioTransmitButtons.xaml @@ -295,6 +298,12 @@ 13.0.1-beta2 + + 3.3.1 + + + 3.3.0 + 3.3.3 @@ -322,7 +331,7 @@ - + - + \ No newline at end of file