我通过使用SKPicture对象记录每个图层的Draw Commands来解决了这个问题,并使用后台渲染线程将它们绘制回SKGLControl,然后使用GUI线程进行绘制。这满足了我的所有要求:允许多个绘画图层、使用后台线程渲染、仅渲染需要更新的图层、使用GPU加速进行绘制,在最大化的4K窗口下运行非常快。
得到的经验教训
途中我学到了一些课程,他们给我带来了很多困惑...
有在线例子使用了带有GPU加速的OpenTK.GLControl以及内置GPU加速的SkiaSharp.Views.Desktop.SKGLControl。SKGLControl绝对是此任务的正确控件。GLControl 对 DrawCircle 创建正方形并由于FramebufferBinding和StencilBits的问题而拒绝渲染任何曲线——我放弃了。对于SKPicture对象,它也比SKGLControl慢。
SKGLControl不需要也不喜欢使用SwapBuffers或Canvas.Flush,这是GLControl所需的。这会导致SKGLControl的绘图出现闪烁和故障,这就是为什么我与GLControl发生冲突的原因。当我使用SKGLControl重建项目并摆脱SwapBuffers和Canvas.Flush时,一切开始正常运行。
对于Surface和Canvas的引用不应保留超过一个PaintSurface周期。SKPicture是可以让您存储每个图层的绘图命令并一遍又一遍地播放它们的神奇对象。这与生成像素光栅的SKBitmap或SKImage不同,而只记录了Draw commands。在多线程环境中,我无法让SKBitmap或SKImage表现出GPU加速。SKPicture非常适合此任务。
Paint事件和PaintSurface事件对于SKGLControl有所不同。默认情况下,应该使用PaintSurface事件,其具有GPU加速功能。
工作示例代码
下面是一个多层、多线程、GPU加速的SkiaSharp绘画的完全功能演示
此示例创建4个绘画图层:
使用后台线程绘制(渲染)各个图层,然后使用GUI线程将它们绘制到SKGLControl上。每个图层只在需要时进行渲染,但所有图层都会在每个PaintSurface事件中进行绘制。
![enter image description here](https://istack.dev59.com/yGg8d.webp)
尝试代码:
- 在Visual Studio中创建一个新的C# WinForms项目。
- 添加NuGet包:“SkiaSharp.Views.WindowsForms”。这将自动添加“SkiaSharp”和“SkiaSharp.Views.Desktop.Common”。
- 将SkiaSharp.Views.Desktop.SKGLControl添加到Form1。将其命名为“skglControl1”
- 为skglControl1设置Dock为“Fill”,使其填充Form1。
- 将下面的代码复制到Form1中:
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
{
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();
public Form1()
{
InitializeComponent();
}
protected override void OnLoad(EventArgs e)
{
base.OnLoad(e);
this.Text = "SkiaSharp Demo - Multi-Layer, Multi-Threaded, GPU Accelerated";
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");
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);
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;
skglControl1.PaintSurface += SkglControl1_PaintSurface;
skglControl1.Resize += SkglControl1_Resize;
skglControl1.MouseMove += SkglControl1_MouseMove;
skglControl1.MouseDoubleClick += SkglControl1_MouseDoubleClick;
m_RenderThread = new Thread(RenderLoopMethod);
m_ThreadGate = new AutoResetEvent(false);
m_RenderThread.Start();
}
protected override void OnClosing(CancelEventArgs e)
{
m_KeepSwimming = false;
m_ThreadGate.Set();
base.OnClosing(e);
}
private void SkglControl1_PaintSurface(object sender, SkiaSharp.Views.Desktop.SKPaintGLSurfaceEventArgs e)
{
e.Surface.Canvas.Clear(SKColors.Black);
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);
}
}
private void SkglControl1_Resize(object sender, EventArgs e)
{
foreach (var layer in m_Layers)
{
layer.Invalidate();
}
UpdateDrawing();
}
private void SkglControl1_MouseMove(object sender, MouseEventArgs e)
{
m_MousePos = e.Location.ToSKPoint();
if (e.Button == MouseButtons.Left)
{
m_Layer_Data.Invalidate();
}
if (e.Location != m_PrevMouseLoc)
{
m_PrevMouseLoc = e.Location;
m_Layer_Overlay.Invalidate();
}
UpdateDrawing();
}
private void SkglControl1_MouseDoubleClick(object sender, MouseEventArgs e)
{
m_ShowGrid = !m_ShowGrid;
m_Layer_Grid.Invalidate();
UpdateDrawing();
}
public void UpdateDrawing()
{
m_ThreadGate.Set();
}
private void RenderLoopMethod()
{
while (m_KeepSwimming)
{
DrawLayers();
skglControl1.Invalidate();
Application.DoEvents();
m_ThreadGate.WaitOne();
}
}
private void DrawLayers()
{
var clippingBounds = skglControl1.ClientRectangle.ToSKRect();
foreach (var layer in m_Layers)
{
layer.Render(clippingBounds);
}
}
private void Layer_Background_Draw(object sender, EventArgs_Draw e)
{
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);
}
}
private void Layer_Grid_Draw(object sender, EventArgs_Draw e)
{
if (m_ShowGrid)
{
using (var paint = new SKPaint())
{
paint.Color = new SKColor(64, 64, 64);
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 1;
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);
}
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);
}
}
}
}
private void Layer_Data_Draw(object sender, EventArgs_Draw e)
{
e.Canvas.Scale(1, -1);
e.Canvas.Translate(0, -e.Bounds.Height);
var rand = new Random();
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 };
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);
}
using (var paint = new SKPaint())
{
paint.Color = SKColors.Blue;
paint.Style = SKPaintStyle.Stroke;
paint.StrokeWidth = 1;
e.Canvas.DrawRect(barLeft, barBottom, barWidth, barHeight, paint);
}
}
}
private void Layer_Overlay_Draw(object sender, EventArgs_Draw e)
{
using (var paint = new SKPaint())
{
paint.Color = SKColors.Black;
paint.Style = SKPaintStyle.Fill;
var text = m_MousePos.ToString();
SKRect textBounds = new SKRect();
paint.MeasureText(text, ref textBounds);
textBounds = textBounds.Standardized;
textBounds.Location = new SKPoint(m_MousePos.X, m_MousePos.Y - textBounds.Height);
e.Canvas.DrawRect(textBounds, paint);
paint.Color = SKColors.Yellow;
e.Canvas.DrawText(m_MousePos.ToString(), m_MousePos, paint);
}
}
}
public class Layer
{
public event EventHandler<EventArgs_Draw> Draw;
private SKPicture m_Picture = null;
private bool m_IsValid = false;
public Layer(string title)
{
this.Title = title;
}
public string Title { get; set; }
public void Render(SKRect clippingBounds)
{
if (!m_IsValid)
{
using (var recorder = new SKPictureRecorder())
{
recorder.BeginRecording(clippingBounds);
Draw?.Invoke(this, new EventArgs_Draw(recorder.RecordingCanvas, clippingBounds));
m_Picture?.Dispose();
m_Picture = recorder.EndRecording();
this.RenderCount++;
m_IsValid = true;
}
}
}
public int RenderCount { get; private set; }
public void Paint(SKCanvas skglControlCanvas)
{
if (m_Picture != null)
{
skglControlCanvas.DrawPicture(m_Picture);
this.PaintCount++;
}
}
public int PaintCount { get; private set; }
public void Invalidate()
{
m_IsValid = false;
}
}
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;
}
}
}
SkglControl1_PaintSurface
{ layer.Paint(e.Surface.Canvas); } 处崩溃。System.AccessViolationException: '试图读取或写入受保护的内存。这通常是其他内存损坏的指示。' - undefined