[英]C# SkiaSharp OpenTK Winform - How to draw from a background thread?
I am trying to replace GDI+ with SkiaSharp for a data visualization framework that renders multi-layered pannable-zoomable graphs with real-time continuously changing engineering data.我正在尝试用 SkiaSharp 替换 GDI+,作为一个数据可视化框架,该框架使用实时不断变化的工程数据呈现多层可平移-可缩放图形。
In GDI+, the application did this:在 GDI+ 中,应用程序是这样做的:
Everything up to the final image presentation was done in one or more background threads.直到最终图像呈现的所有内容都在一个或多个后台线程中完成。 The GUI thread was only involved to draw the finished image to the PictureBox. GUI 线程仅用于将完成的图像绘制到 PictureBox。 This is important because there are many other GUI controls that need to stay responsive.这很重要,因为还有许多其他 GUI 控件需要保持响应。 This worked great, except it is all CPU based.这很好用,除了它都是基于 CPU 的。 Small windows were no problem, but maximizing on a 4K screen would slow down the rendering enough to make the program pretty much unusable.小窗口没有问题,但在 4K 屏幕上最大化会减慢渲染速度,使程序几乎无法使用。
I would like to recreate this concept with GPU accelerated SkiaSharp.我想用 GPU 加速的 SkiaSharp 重新创建这个概念。
I tried creating dozens of different test programs and I keep getting Cross-Thread access violations, or nothing showing on the screen, or hard crashes.我尝试创建了几十个不同的测试程序,但我不断遇到跨线程访问冲突,或者屏幕上没有任何显示,或者严重崩溃。 Instead of posting code, let me ask some basic questions:让我问一些基本问题,而不是发布代码:
Questions:问题:
Any help defining the approach and the dos and don'ts would be greatly appreciated !!任何定义方法和注意事项的帮助将不胜感激!
I figured out how to get this to work using SKPicture objects to record the Draw Commands from each layer using a background rendering thread, then painting them back to an SKGLControl using the GUI thread.我想出了如何使用 SKPicture 对象来使用后台渲染线程记录每一层的绘制命令,然后使用 GUI 线程将它们绘制回 SKGLControl 的方法。 This satisfies all of my requirements: It allows for multiple drawing layers, renders with a background thread, renders only the layers that need updates, paints with GPU acceleration, and is extremely fast for a maximized 4K window.这满足了我的所有要求:它允许多个绘图层,使用后台线程渲染,仅渲染需要更新的层,使用 GPU 加速绘制,并且对于最大化的 4K 窗口非常快。
There are a few lessons that I learned along the way that were causing a lot of confusion for me...一路上我学到的一些教训给我带来了很多困惑......
There are examples online of using an OpenTK.GLControl with GPU acceleration, and there are examples using the SkiaSharp.Views.Desktop.SKGLControl which has built in GPU acceleration.网上有使用带有 GPU 加速的 OpenTK.GLControl 的示例,也有使用内置 GPU 加速的 SkiaSharp.Views.Desktop.SKGLControl 的示例。 The SKGLControl is definitely the correct control for this task. SKGLControl 绝对是此任务的正确控件。 The GLControl was creating squares for DrawCircle and refusing to render any curves due to issues with the FramebufferBinding and StencilBits ?!?由于 FramebufferBinding 和 StencilBits 的问题,GLControl 正在为 DrawCircle 创建正方形并拒绝渲染任何曲线?!? - I gave up on it. - 我放弃了。 It is also slower than the SKGLControl for SKPicture objects.它也比 SKPicture 对象的 SKGLControl 慢。
The SKGLControl does not need nor like the use of SwapBuffers or Canvas.Flush, which are required for the GLControl. SKGLControl 不需要也不喜欢使用 GLControl 所需的 SwapBuffers 或 Canvas.Flush。 This was causing strobing and glitching of the drawings for SKGLControl, which is why I went off in the weeds fighting with the GLControl.这导致 SKGLControl 的绘图出现频闪和故障,这就是我在与 GLControl 斗争的杂草中离开的原因。 When I rebuilt the project with SKGLControl and got rid of SwapBuffers and Canvas.Flush, everything started behaving.当我用 SKGLControl 重建项目并摆脱 SwapBuffers 和 Canvas.Flush 时,一切都开始正常了。
References to Surfaces and Canvases should not be held past one PaintSurface cycle.对 Surfaces 和 Canvases 的引用不应超过一个 PaintSurface 循环。 The SKPicture is the magical object that will let you store the drawing commands for each layer and play them back again and again. SKPicture 是一个神奇的对象,它可以让您存储每个图层的绘图命令并一次又一次地播放它们。 This is different from an SKBitmap or SKImage which are generating pixel rasters instead of just recording the Draw commands.这与 SKBitmap 或 SKImage 不同,SKBitmap 或 SKImage 生成像素栅格,而不仅仅是记录 Draw 命令。 I couldn't get SKBitmap or SKImage to behave in a multithreaded environment and still be GPU accelerated.我无法让 SKBitmap 或 SKImage 在多线程环境中运行并且仍然是 GPU 加速的。 SKPicture works great for this. SKPicture 对此非常有用。
There is a difference between the Paint event and the PaintSurface event for the SKGLControl. SKGLControl 的 Paint 事件和 PaintSurface 事件之间存在差异。 The PaintSurface event is what should be used and is by default GPU accelerated. PaintSurface 事件是应该使用的,默认情况下是 GPU 加速的。
Below is fully functional demo of a multi-layer, multi-threaded, GPU accelerated SkiaSharp drawing下面是一个多层、多线程、GPU 加速的 SkiaSharp 绘图的全功能演示
This example creates 4 drawing layers:此示例创建 4 个绘图层:
The layers are drawn ( rendered ) using a background thread, then painted to an SKGLControl using the GUI thread.使用后台线程绘制(渲染)图层,然后使用 GUI 线程绘制到 SKGLControl。 Each layer is only rendered when needed, but all layers are painted with each PaintSurface event.每个图层仅在需要时渲染,但所有图层都使用每个 PaintSurface 事件绘制。
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;
using SkiaSharp;
using SkiaSharp.Views.Desktop;
namespace SkiaSharp_Multi_Layer_GPU
{
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// ------- -------
// ------- WinForm - Form 1 -------
// ------- -------
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
public partial class Form1 : Form
{
private Thread m_RenderThread = null;
private AutoResetEvent m_ThreadGate = null;
private List<Layer> m_Layers = null;
private Layer m_Layer_Background = null;
private Layer m_Layer_Grid = null;
private Layer m_Layer_Data = null;
private Layer m_Layer_Overlay = null;
private bool m_KeepSwimming = true;
private SKPoint m_MousePos = new SKPoint();
private bool m_ShowGrid = true;
private Point m_PrevMouseLoc = new Point();
// ---------------------------
// --- Form1 - Constructor ---
// ---------------------------
public Form1()
{
InitializeComponent();
}
// ------------------------------
// --- Event - Form1 - OnLoad ---
// ------------------------------
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
// Set the title of the Form
this.Text = "SkiaSharp Demo - Multi-Layer, Multi-Threaded, GPU Accelerated";
// Create layers to draw on, each with a dedicated SKPicture
m_Layer_Background = new Layer("Background Layer");
m_Layer_Grid = new Layer("Grid Layer");
m_Layer_Data = new Layer("Data Layer");
m_Layer_Overlay = new Layer("Overlay Layer");
// Create a collection for the drawing layers
m_Layers = new List<Layer>();
m_Layers.Add(m_Layer_Background);
m_Layers.Add(m_Layer_Grid);
m_Layers.Add(m_Layer_Data);
m_Layers.Add(m_Layer_Overlay);
// Subscribe to the Draw Events for each layer
m_Layer_Background.Draw += Layer_Background_Draw;
m_Layer_Grid.Draw += Layer_Grid_Draw;
m_Layer_Data.Draw += Layer_Data_Draw;
m_Layer_Overlay.Draw += Layer_Overlay_Draw;
// Subscribe to the SKGLControl events
skglControl1.PaintSurface += SkglControl1_PaintSurface;
skglControl1.Resize += SkglControl1_Resize;
skglControl1.MouseMove += SkglControl1_MouseMove;
skglControl1.MouseDoubleClick += SkglControl1_MouseDoubleClick;
// Create a background rendering thread
m_RenderThread = new Thread(RenderLoopMethod);
m_ThreadGate = new AutoResetEvent(false);
// Start the rendering thread
m_RenderThread.Start();
}
// ---------------------------------
// --- Event - Form1 - OnClosing ---
// ---------------------------------
protected override void OnClosing(CancelEventArgs e)
{
// Let the rendering thread terminate
m_KeepSwimming = false;
m_ThreadGate.Set();
base.OnClosing(e);
}
// --------------------------------------------
// --- Event - SkglControl1 - Paint Surface ---
// --------------------------------------------
private void SkglControl1_PaintSurface(object sender, SkiaSharp.Views.Desktop.SKPaintGLSurfaceEventArgs e)
{
// Clear the Canvas
e.Surface.Canvas.Clear(SKColors.Black);
// Paint each pre-rendered layer onto the Canvas using this GUI thread
foreach (var layer in m_Layers)
{
layer.Paint(e.Surface.Canvas);
}
using (var paint = new SKPaint())
{
paint.Color = SKColors.LimeGreen;
for (int i = 0; i < m_Layers.Count; i++)
{
var layer = m_Layers[i];
var text = $"{layer.Title} - Renders = {layer.RenderCount}, Paints = {layer.PaintCount}";
var textLoc = new SKPoint(10, 10 + (i * 15));
e.Surface.Canvas.DrawText(text, textLoc, paint);
}
paint.Color = SKColors.Cyan;
e.Surface.Canvas.DrawText("Click-Drag to update bars.", new SKPoint(10, 80), paint);
e.Surface.Canvas.DrawText("Double-Click to show / hide grid.", new SKPoint(10, 95), paint);
e.Surface.Canvas.DrawText("Resize to update all.", new SKPoint(10, 110), paint);
}
}
// -------------------------------------
// --- Event - SkglControl1 - Resize ---
// -------------------------------------
private void SkglControl1_Resize(object sender, EventArgs e)
{
// Invalidate all of the Layers
foreach (var layer in m_Layers)
{
layer.Invalidate();
}
// Start a new rendering cycle to redraw all of the layers.
UpdateDrawing();
}
// -----------------------------------------
// --- Event - SkglControl1 - Mouse Move ---
// -----------------------------------------
private void SkglControl1_MouseMove(object sender, MouseEventArgs e)
{
// Save the mouse position
m_MousePos = e.Location.ToSKPoint();
// If Left-Click Drag, draw new bars
if (e.Button == MouseButtons.Left)
{
// Invalidate the Data Layer to draw a new random set of bars
m_Layer_Data.Invalidate();
}
// If Mouse Move, draw new mouse coordinates
if (e.Location != m_PrevMouseLoc)
{
// Remember the previous mouse location
m_PrevMouseLoc = e.Location;
// Invalidate the Overlay Layer to show the new mouse coordinates
m_Layer_Overlay.Invalidate();
}
// Start a new rendering cycle to redraw any invalidated layers.
UpdateDrawing();
}
// -------------------------------------------------
// --- Event - SkglControl1 - Mouse Double Click ---
// -------------------------------------------------
private void SkglControl1_MouseDoubleClick(object sender, MouseEventArgs e)
{
// Toggle the grid visibility
m_ShowGrid = !m_ShowGrid;
// Invalidate only the Grid Layer.
m_Layer_Grid.Invalidate();
// Start a new rendering cycle to redraw any invalidated layers.
UpdateDrawing();
}
// ----------------------
// --- Update Drawing ---
// ----------------------
public void UpdateDrawing()
{
// Unblock the rendering thread to begin a render cycle. Only the invalidated
// Layers will be re-rendered, but all will be repainted onto the SKGLControl.
m_ThreadGate.Set();
}
// --------------------------
// --- Render Loop Method ---
// --------------------------
private void RenderLoopMethod()
{
while (m_KeepSwimming)
{
// Draw any invalidated layers using this Render thread
DrawLayers();
// Invalidate the SKGLControl to run the PaintSurface event on the GUI thread
// The PaintSurface event will Paint the layer stack to the SKGLControl
skglControl1.Invalidate();
// DoEvents to ensure that the GUI has time to process
Application.DoEvents();
// Block and wait for the next rendering cycle
m_ThreadGate.WaitOne();
}
}
// -------------------
// --- Draw Layers ---
// -------------------
private void DrawLayers()
{
// Iterate through the collection of layers and raise the Draw event for each layer that is
// invalidated. Each event handler will receive a Canvas to draw on along with the Bounds for
// the Canvas, and can then draw the contents of that layer. The Draw commands are recorded and
// stored in an SKPicture for later playback to the SKGLControl. This method can be called from
// any thread.
var clippingBounds = skglControl1.ClientRectangle.ToSKRect();
foreach (var layer in m_Layers)
{
layer.Render(clippingBounds);
}
}
// -----------------------------------------
// --- Event - Layer - Background - Draw ---
// -----------------------------------------
private void Layer_Background_Draw(object sender, EventArgs_Draw e)
{
// Create a diagonal gradient fill from Blue to Black to use as the background
var topLeft = new SKPoint(e.Bounds.Left, e.Bounds.Top);
var bottomRight = new SKPoint(e.Bounds.Right, e.Bounds.Bottom);
var gradColors = new SKColor[2] { SKColors.DarkBlue, SKColors.Black };
using (var paint = new SKPaint())
using (var shader = SKShader.CreateLinearGradient(topLeft, bottomRight, gradColors, SKShaderTileMode.Clamp))
{
paint.Shader = shader;
paint.Style = SKPaintStyle.Fill;
e.Canvas.DrawRect(e.Bounds, paint);
}
}
// -----------------------------------
// --- Event - Layer - Grid - Draw ---
// -----------------------------------
private void Layer_Grid_Draw(object sender, EventArgs_Draw e)
{
if (m_ShowGrid)
{
// Draw a 25x25 grid of gray lines
using (var paint = new SKPaint())
{
paint.Color = new SKColor(64, 64, 64); // Very dark gray
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 1;
// Draw the Horizontal Grid Lines
for (int i = 0; i < 50; i++)
{
var y = e.Bounds.Height * (i / 25f);
var leftPoint = new SKPoint(e.Bounds.Left, y);
var rightPoint = new SKPoint(e.Bounds.Right, y);
e.Canvas.DrawLine(leftPoint, rightPoint, paint);
}
// Draw the Vertical Grid Lines
for (int i = 0; i < 50; i++)
{
var x = e.Bounds.Width * (i / 25f);
var topPoint = new SKPoint(x, e.Bounds.Top);
var bottomPoint = new SKPoint(x, e.Bounds.Bottom);
e.Canvas.DrawLine(topPoint, bottomPoint, paint);
}
}
}
}
// -----------------------------------
// --- Event - Layer - Date - Draw ---
// -----------------------------------
private void Layer_Data_Draw(object sender, EventArgs_Draw e)
{
// Draw a simple bar graph
// Flip the Y-Axis so that zero is on the bottom
e.Canvas.Scale(1, -1);
e.Canvas.Translate(0, -e.Bounds.Height);
var rand = new Random();
// Create 25 red / yellow gradient bars of random length
for (int i = 0; i < 25; i++)
{
var barWidth = e.Bounds.Width / 25f;
var barHeight = rand.Next((int)(e.Bounds.Height * 0.65d));
var barLeft = (i + 0) * barWidth;
var barRight = (i + 1) * barWidth;
var barTop = barHeight;
var barBottom = 0;
var topLeft = new SKPoint(barLeft, barTop);
var bottomRight = new SKPoint(barRight, barBottom);
var gradColors = new SKColor[2] { SKColors.Yellow, SKColors.Red };
// Draw each bar with a gradient fill
using (var paint = new SKPaint())
using (var shader = SKShader.CreateLinearGradient(topLeft, bottomRight, gradColors, SKShaderTileMode.Clamp))
{
paint.Style = SKPaintStyle.Fill;
paint.StrokeWidth = 1;
paint.Shader = shader;
e.Canvas.DrawRect(barLeft, barBottom, barWidth, barHeight, paint);
}
// Draw the border of each bar
using (var paint = new SKPaint())
{
paint.Color = SKColors.Blue;
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 1;
e.Canvas.DrawRect(barLeft, barBottom, barWidth, barHeight, paint);
}
}
}
// --------------------------------------
// --- Event - Layer - Overlay - Draw ---
// --------------------------------------
private void Layer_Overlay_Draw(object sender, EventArgs_Draw e)
{
// Draw the mouse coordinate text next to the cursor
using (var paint = new SKPaint())
{
// Configure the Paint to draw a black rectangle behind the text
paint.Color = SKColors.Black;
paint.Style = SKPaintStyle.Fill;
// Measure the bounds of the text
var text = m_MousePos.ToString();
SKRect textBounds = new SKRect();
paint.MeasureText(text, ref textBounds);
// Fix the inverted height value from the MeaureText
textBounds = textBounds.Standardized;
textBounds.Location = new SKPoint(m_MousePos.X, m_MousePos.Y - textBounds.Height);
// Draw the black filled rectangle where the text will go
e.Canvas.DrawRect(textBounds, paint);
// Change the Paint to yellow
paint.Color = SKColors.Yellow;
// Draw the mouse coordinates text
e.Canvas.DrawText(m_MousePos.ToString(), m_MousePos, paint);
}
}
}
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// ------- -------
// ------- Class - Layer -------
// ------- -------
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
public class Layer
{
// The Draw event that the background rendering thread will use to draw on the SKPicture Canvas.
public event EventHandler<EventArgs_Draw> Draw;
// The finished recording - Used to play back the Draw commands to the SKGLControl from the GUI thread
private SKPicture m_Picture = null;
// A flag that indicates if the Layer is valid, or needs to be redrawn.
private bool m_IsValid = false;
// ---------------------------
// --- Layer - Constructor ---
// ---------------------------
public Layer(string title)
{
this.Title = title;
}
// -------------
// --- Title ---
// -------------
public string Title { get; set; }
// --------------
// --- Render ---
// --------------
// Raises the Draw event and records any drawing commands to an SKPicture for later playback.
// This can be called from any thread.
public void Render(SKRect clippingBounds)
{
// Only redraw the Layer if it has been invalidated
if (!m_IsValid)
{
// Create an SKPictureRecorder to record the Canvas Draw commands to an SKPicture
using (var recorder = new SKPictureRecorder())
{
// Start recording
recorder.BeginRecording(clippingBounds);
// Raise the Draw event. The subscriber can then draw on the Canvas provided in the event
// and the commands will be recorded for later playback.
Draw?.Invoke(this, new EventArgs_Draw(recorder.RecordingCanvas, clippingBounds));
// Dispose of any previous Pictures
m_Picture?.Dispose();
// Create a new SKPicture with recorded Draw commands
m_Picture = recorder.EndRecording();
this.RenderCount++;
m_IsValid = true;
}
}
}
// --------------------
// --- Render Count ---
// --------------------
// Gets the number of times that this Layer has been rendered
public int RenderCount { get; private set; }
// -------------
// --- Paint ---
// -------------
// Paints the previously recorded SKPicture to the provided skglControlCanvas. This basically plays
// back the draw commands from the last Render. This should be called from the SKGLControl.PaintSurface
// event using the GUI thread.
public void Paint(SKCanvas skglControlCanvas)
{
if (m_Picture != null)
{
// Play back the previously recorded Draw commands to the skglControlCanvas using the GUI thread
skglControlCanvas.DrawPicture(m_Picture);
this.PaintCount++;
}
}
// --------------------
// --- Render Count ---
// --------------------
// Gets the number of times that this Layer has been painted
public int PaintCount { get; private set; }
// ------------------
// --- Invalidate ---
// ------------------
// Forces the Layer to be redrawn with the next rendering cycle
public void Invalidate()
{
m_IsValid = false;
}
}
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// ------- -------
// ------- EventArgs - Draw -------
// ------- -------
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
public class EventArgs_Draw : EventArgs
{
public SKRect Bounds { get; set; }
public SKCanvas Canvas { get; set; }
public EventArgs_Draw(SKCanvas canvas, SKRect bounds)
{
this.Canvas = canvas;
this.Bounds = bounds;
}
}
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.