Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SKGLElement to SkiaSharp.Views.WPF #2317

Merged
merged 19 commits into from
Mar 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
221 changes: 221 additions & 0 deletions source/SkiaSharp.Views/SkiaSharp.Views.WPF/SKGLElement.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using OpenTK.Graphics;
using System.Windows.Media.Media3D;
using SkiaSharp.Views.Desktop;
using OpenTK.Wpf;
using SkiaSharp;
using OpenTK.Graphics.OpenGL;
using OpenTK.Platform.Windows;
using OpenTK;
using System.Windows.Interop;
using OpenTK.Platform;
#if NETCOREAPP || NET
using OpenTK.Mathematics;
#endif

namespace SkiaSharp.Views.WPF
{
[DefaultEvent("PaintSurface")]
[DefaultProperty("Name")]
public class SKGLElement : GLWpfControl, IDisposable
{
private const SKColorType colorType = SKColorType.Rgba8888;
private const GRSurfaceOrigin surfaceOrigin = GRSurfaceOrigin.BottomLeft;

private bool designMode;

private GRContext grContext;
private GRGlFramebufferInfo glInfo;
private GRBackendRenderTarget renderTarget;
private SKSurface surface;
private SKCanvas canvas;

private SKSizeI lastSize;

public SKGLElement()
: base()
{
Initialize();
}

private void Initialize()
{
designMode = DesignerProperties.GetIsInDesignMode(this);
var settings = new GLWpfControlSettings() { MajorVersion = 2, MinorVersion = 1, RenderContinuously = false };

this.Render += OnPaint;

this.Loaded += SKGLElement_Loaded;
this.Unloaded += SKGLElement_Unloaded;

#if NETCOREAPP
this.RegisterToEventsDirectly = false;
#endif

Start(settings);
}

private void SKGLElement_Unloaded(object sender, RoutedEventArgs e)
{
Release();
}
private void SKGLElement_Loaded(object sender, RoutedEventArgs e)
{
InvalidateVisual();
}

public SKSize CanvasSize => lastSize;

public GRContext GRContext => grContext;

[Category("Appearance")]
public event EventHandler<SKPaintGLSurfaceEventArgs> PaintSurface;

private SKSizeI GetSize()
{
var currentWidth = ActualWidth;
var currentHeight = ActualHeight;

if (currentWidth < 0 ||
currentHeight < 0)
{
currentWidth = 0;
currentHeight = 0;
}

PresentationSource source = PresentationSource.FromVisual(this);

double dpiX = 1.0;
double dpiY = 1.0;
if (source != null)
{
dpiX = source.CompositionTarget.TransformToDevice.M11;
dpiY = source.CompositionTarget.TransformToDevice.M22;
}

return new SKSizeI((int)(currentWidth * dpiX), (int)(currentHeight * dpiY));
}

protected override void OnRender(DrawingContext drawingContext)
{
if (grContext != null)
{
grContext.ResetContext();
Copy link
Contributor

@mattleibow mattleibow Mar 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am thinking about this one... GL has a "make current" concept... I wonder if the draw method is supposed to make the context current before drawing.

I see this which looks familiar, but I have no idea how to get access to this one: https://github.com/opentk/GLWpfControl/blob/516bf697e9c786ab3432156d75e1dfca67d3a18c/src/GLWpfControl/DXGLContext.cs#L127

In the FAQ:

How do I control OpenGL contexts?

On many OSes, there is the notion of an OpenGL context, a container that holds all of the allocated OpenGL resources like textures and shaders and buffers. There is also typically the concept of a current context — there’s usually only one context “active” at a time, and you have to switch between them if you have more than one.

OpenTK exposes this through NativeWindow in the form of its Context property and its MakeCurrent() method. Context is an IGraphicsContext, and includes low-level OpenGL operations like MakeCurrent() and MakeNoneCurrent() and SwapBuffers().

The graphics context is created automatically for you when a NativeWindow is created, so the only thing you may need to do is occasionally invoke MakeCurrent() or MakeNoneCurrent().

(If you only have one OpenGL context, you can ignore all of this.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I tried before was to take a variant of that code you linked from inside OpenTK (that creates the shared context) to create additional contexts, and one per SKGLElement, Then each would make current before rendering the frame. IIRC that worked. But it required essentially recreating the context creation outside of OpenTK. I can't remember if I hit another limitation with it. This would probably work, I'm guessing, but would mean we are creating one GL context per control, and I'm not sure if that is desirable? This may be what the WF is doing under the covers though due to each being a separate HWND presumably? Not sure.

Anyhow, using the shared gl context from each SKGLElement was not working though without resetting the graphics context, since there were left over bits of state (maybe clipping stages?) that were crosstalking between the SKElements.

I'll see if I can find the old code that I was trying with the multiple gl contexts. I believe I posted it to discord in the past when I was asking for advice. The GL context creation was some variant of what is happening in that code you linked though.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can merge this with the release for now, and then in a separate PR benchmark and see if switching context is better. I don't think we need to make a new context, but somehow we need to make sure the one for the control is make current before drawing.

When you create a new GRContext, it calls make current internally, so this is what is probably making it work.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe there is a bug in the GL WPF Control, so we can also investigate that path. But, that can be done later.

}
mattleibow marked this conversation as resolved.
Show resolved Hide resolved
base.OnRender(drawingContext);
}

protected virtual void OnPaint(TimeSpan e)
{
if (disposed)
{
return;
}
if (designMode)
{
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 = GetSize();

GL.ClearColor(Color4.Transparent);
GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit | ClearBufferMask.StencilBufferBit);

// 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));
}

// update the control
canvas.Flush();
}

protected virtual void OnPaintSurface(SKPaintGLSurfaceEventArgs e)
{
// invoke the event
PaintSurface?.Invoke(this, e);
}

private bool disposed = false;


protected virtual void Dispose(bool disposing)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WPF UI worlds don't really use Dispose as a mechanism for doing things. Unload is resetting already? I am not 100% confident of how things work in the worlds of heavy graphics objects.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My recollection is that I was having an issue if the window was closed before the OpenTK stuff was disposed, which is why I was trying to hook the window close... But I haven't been able to repro revisiting it here. Maybe it had to do with me forcing OpenTK to create more than one glcontext associated with the window.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its conceivable that opentk manages any resources associated with the shared context of the shared window well enough that this code isn't necessary if I'm not spinning up other contexts.... although, now that I'm typing this, I wonder if I was prompted by any multi window wpf scenarios. Been a while since I was first putting this together at this point.

{
if (disposed)
{
return;
}

Release();

disposed = true;
}

private void Release()
{
canvas = null;
surface?.Dispose();
surface = null;
renderTarget?.Dispose();
renderTarget = null;
grContext?.Dispose();
grContext = null;
}

public void Dispose()
{
Dispose(true);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,17 @@
<PackagingGroup>SkiaSharp.Views.WPF</PackagingGroup>
<DefineConstants>$(DefineConstants);__DESKTOP__;__WPF__</DefineConstants>
<Title>SkiaSharp Views &amp; Layers for Windows Presentation Foundation (WPF)</Title>
<SignAssembly Condition="'$(TargetFramework)' != 'net462'">false</SignAssembly>
<PackageTags>wpf</PackageTags>
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net462'">
<PackageReference Include="OpenTK" Version="3.3.1" NoWarn="NU1701" />
<PackageReference Include="OpenTK.GLWpfControl" Version="3.3.0" NoWarn="NU1701" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' != 'net462'">
<PackageReference Include="OpenTK" Version="4.3.0" NoWarn="NU1701" />
<PackageReference Include="OpenTK.GLWpfControl" Version="4.2.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\binding\SkiaSharp\SkiaSharp.csproj" />
<ProjectReference Include="..\SkiaSharp.Views.Desktop.Common\SkiaSharp.Views.Desktop.Common.csproj" />
Expand Down
Loading