【问题标题】:C# SkiaSharp OpenTK Winform - How to draw from a background thread?C# SkiaSharp OpenTK Winform - 如何从后台线程中绘制?
【发布时间】:2021-02-20 13:04:35
【问题描述】:

我正在尝试将 GDI+ 替换为 SkiaSharp,以获得数据可视化框架,该框架使用实时不断变化的工程数据呈现多层可平移-可缩放图。

在 GDI+ 中,应用程序执行以下操作:

  • 创建了一组具有透明背景的绘图层,通常是一个网格层、一个或多个数据层,以及一个用于光标信息和突出显示的覆盖层,每个层都有一个单独的位图支持。
  • 在渲染循环后台线程中,仅使用 GDI+ 重绘了每个渲染循环需要更新的层(位图)。这可能需要数千条计算和转换的线、矩形和文本来创建热图、波形、直方图、数据标签等。
  • 堆栈中的每个绘图层随后将由后台线程 BitBlt'd 到复合位图
  • 最终的复合位图随后会以最高 30 fps 的速度绘制到 GUI 线程中的 WinForm PictureBox。

直到最终图像呈现的所有工作都在一个或多个后台线程中完成。 GUI 线程仅参与将完成的图像绘制到 PictureBox。这很重要,因为还有许多其他 GUI 控件需要保持响应。这很好用,除了它都是基于 CPU 的。小窗口没有问题,但在 4K 屏幕上最大化会减慢渲染速度,足以使程序几乎无法使用。

我想用 GPU 加速的 SkiaSharp 重新创建这个概念。

我尝试创建了几十个不同的测试程序,但我不断遇到跨线程访问冲突,或者屏幕上没有显示,或者硬崩溃。让我问一些基本问题,而不是发布代码:

问题:

  • 您将如何创建此框架? SkiaSharp 甚至可以做到这一点吗?
  • 我的每个图层类都应该维护一个 SKSurface、SKCanvas、SKImage 或 SKBitmap 吗? - 同样,如果在当前循环中不需要重新绘制图层,则该图层需要保留之前绘制的内容以用于下一个合成图像。
  • GUI 线程上需要一个 GLControl 和 GRContext 来显示最终的合成图像,但是否应该有另一个单独的 GRContext 供后台渲染线程使用? - 如何使用 GPU 加速进行创作?
  • 是否有人可以指出类似概念的任何工作示例? (GPU 加速从后台线程到 GLControl 的渲染)
  • 我应该只使用隐藏在背景中的 SkiaSharp,并使用带有 PictureBox 的 GDI+ BitBlt 在屏幕上显示合成图像吗? - 这会解决一些线程问题吗?

任何帮助定义方法和注意事项将不胜感激!

【问题讨论】:

    标签: c# multithreading gpu opentk skiasharp


    【解决方案1】:

    我想出了如何使用 SKPicture 对象使用后台渲染线程记录每个图层的绘制命令,然后使用 GUI 线程将它们绘制回 SKGLControl 来使其工作。这满足了我的所有要求:它允许多个绘图层,使用后台线程进行渲染,仅渲染需要更新的层,使用 GPU 加速进行绘图,并且对于最大化的 4K 窗口非常快。

    经验教训

    我在此过程中学到的一些教训给我带来了很多困惑......

    1. 网上有使用带有 GPU 加速的 OpenTK.GLControl 的示例,也有使用内置 GPU 加速的 SkiaSharp.Views.Desktop.SKGLControl 的示例。 SKGLControl 绝对是此任务的正确控件。由于 FramebufferBinding 和 StencilBits 的问题,GLControl 正在为 DrawCircle 创建正方形并拒绝渲染任何曲线?!? - 我放弃了。对于 SKPicture 对象,它也比 SKGLControl 慢。

    2. SKGLControl 不需要也不喜欢使用 GLControl 所需的 SwapBuffers 或 Canvas.Flush。这导致了 SKGLControl 绘图的频闪和故障,这就是为什么我在与 GLControl 的杂草中奋战的原因。当我使用 SKGLControl 重建项目并摆脱 SwapBuffers 和 Canvas.Flush 时,一切都开始正常运行了。

    3. 对 Surfaces 和 Canvases 的引用不应超过一个 PaintSurface 周期。 SKPicture 是一个神奇的对象,它可以让您存储每个图层的绘图命令并一次又一次地播放它们。这与生成像素栅格而不是仅记录 Draw 命令的 SKBitmap 或 SKImage 不同。我无法让 SKBitmap 或 SKImage 在多线程环境中运行,并且仍然可以进行 GPU 加速。 SKPicture 非常适合这一点。

    4. SKGLControl 的 Paint 事件和 PaintSurface 事件之间存在差异。 PaintSurface 事件是应该使用的,默认情况下是 GPU 加速的。


    工作示例代码

    以下是多层、多线程、GPU 加速的 SkiaSharp 绘图的全功能演示

    这个例子创建了 4 个绘图层:

    • 背景层
    • 网格层
    • 数据层
    • 叠加层

    图层使用后台线程绘制(渲染),然后使用 GUI 线程绘制到 SKGLControl。每个图层仅在需要时渲染,但所有图层都使用每个 PaintSurface 事件进行绘制。

    试用代码:

    1. 在 Visual Studio 中创建一个新的 C# WinForms 项目。
    2. 添加 NuGet 包:“SkiaSharp.Views.WindowsForms”。这将自动添加“SkiaSharp”和“SkiaSharp.Views.Desktop.Common”。
    3. 将 SkiaSharp.Views.Desktop.SKGLControl 添加到 Form1。将其命名为“skglControl1”
    4. 将 skglControl1 的 Dock 设置为“填充”,以便填充 Form1。
    5. 将以下代码复制到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
        {
            // ---------------------------------------------------------------------
            // ---------------------------------------------------------------------
            // -------                                                       -------
            // -------                   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;
                }
            }
        
        }
    
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多