WPF 高性能位图渲染 WriteableBitmap 及其高性能用法示例

原文:WPF 高性能位图渲染 WriteableBitmap 及其高性能用法示例

WPF 渲染框架并没有对外提供多少可以完全控制渲染的部分,目前可以做的有:

  • D3DImage,用来承载使用 DirectX 各个版本渲染内容的控件
  • WriteableBitmap,通过一段内存空间来指定如何渲染一个位图的图片
  • HwndHost,通过承载一个子窗口以便能叠加任何种类渲染的控件

本文将解释如何最大程度压榨 WriteableBitmap 在 WPF 下的性能。


本文内容

    • 如何使用 WriteableBitmap
    • 启用不安全代码
    • 启用帧率测试
      • 4K 脏区
      • 小脏区
      • 无脏区
      • 不渲染
      • 脏区大小与 CPU 占用率之间的关系
    • 启用基准测试(Benchmark)
      • 使用 `CopyMemory` 拷贝内存
      • 使用 `MoveMemory` 移动内存
      • 使用 `Buffer.MemoryCopy` 拷贝内存
      • 自己写 for 循环
      • 基准测试数据
    • 结论和使用建议
    • WriteableBitmap 渲染原理

如何使用 WriteableBitmap

创建一个新的 WPF 项目,然后我们在 MainWindow.xaml 中编写一点可以用来显示 WriteableBitmap 的代码:

<Window x:Class="Walterlv.Demo.HighPerformanceBitmap.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Walterlv.Demo.HighPerformanceBitmap"
        Title="WriteableBitmap - walterlv" SizeToContent="WidthAndHeight">
    <Grid>
        <Image x:Name="Image" Width="1280" Height="720" />
    </Grid>
</Window>


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

为了评估其性能,我决定绘制和渲染 4K 品质的位图,并通过以下步骤来评估:

  1. 使用 CompositionTarget.Rendering 逐帧渲染以评估其渲染帧率
  2. 使用 Benchmark 基准测试来测试内部各种不同方法的性能差异

于是,在 MainWindow.xaml.cs 中添加一些测试用的修改 WriteableBitmap 的代码:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace Walterlv.Demo.HighPerformanceBitmap
{
    public partial class MainWindow : Window
    {
        private readonly WriteableBitmap _bitmap;

        public MainWindow()
        {
            InitializeComponent();

            _bitmap = new WriteableBitmap(3840, 2160, 96.0, 96.0, PixelFormats.Pbgra32, null);
            Image.Source = _bitmap;
            CompositionTarget.Rendering += CompositionTarget_Rendering;
        }

        private void CompositionTarget_Rendering(object sender, EventArgs e)
        {
            var width = _bitmap.PixelWidth;
            var height = _bitmap.PixelHeight;

            _bitmap.Lock();

            // 在这里添加绘制位图的逻辑。

            _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
            _bitmap.Unlock();
        }
    }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

注意,我留了一行注释说即将添加绘制位图的逻辑,接下来我们的主要内容将从此展开。

启用不安全代码

为了获取最佳性能,我们需要开启不安全代码。为此,你需要修改一下你的项目属性。

你可以阅读我的另一篇博客了解如何启用不安全代码:

简单点说就是在你的项目文件中添加下面这一行:

    <Project Sdk="Microsoft.NET.Sdk">
      <PropertyGroup>
        <TargetFramework>netcoreapp3.0</TargetFramework>
++      <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
      </PropertyGroup>
    </Project>


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

启用帧率测试

接下来,我们需要添加一点点代码来评估 WriteableBitmap 的性能:

++  private readonly byte[] _empty4KBitmapArray = new byte[3840 * 2160 * 4];

--  private void CompositionTarget_Rendering(object sender, EventArgs e)
++  private unsafe void CompositionTarget_Rendering(object sender, EventArgs e)
    {
        var width = _bitmap.PixelWidth;
        var height = _bitmap.PixelHeight;

        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          Buffer.MemoryCopy(ptr, _bitmap.BackBuffer.ToPointer(), _empty4KBitmapArray.Length, _empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

嗯,就是将一个空的 4K 大小的数组中的内容复制到 WriteableBitmap 的位图缓存中。

4K 脏区

虽然我们看不到任何可变的修改,不过 WriteableBitmap 可不这么认为。因为我们调用了 AddDirtyRect 将整个位图空间都加入到了脏区中,这样 WPF 会重新渲染整幅位图。

Visual Studio 中看到的 CPU 占用率大约维持在 16% 左右(跟具体机器相关);并且除了一开始启动的时候之外,完全没有 GC(这点很重要),内存稳定在一个值上不再变化。

也只有本文一开始提及的三种方法才可能做到渲染任何可能的图形的时候没有 GC

查看界面渲染帧率可以发现跑满 60 帧没有什么问题(跟具体机器相关)。

小脏区

现在,我们把脏区的区域缩小为 100*100,同样看性能数据。

--  _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
++  _bitmap.AddDirtyRect(new Int32Rect(0, 0, 100, 100));


  • 1
  • 2

可以发现 CPU 占用降低到一半(确实是大幅降低,但是跟像素数量并不成比例);内存没有变化(废话,4K 图像是确定的);帧率没有变化(废话,只要性能够,帧率就是满的)。

无脏区

现在,我们将脏区清零。

--  _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
++  _bitmap.AddDirtyRect(new Int32Rect(0, 0, 0, 0));


  • 1
  • 2

在完全没有脏区的时候,CPU 占用直接降为 0,这个性能提升还是非常恐怖的。

不渲染

如果我们不把 WriteableBitmap 设置为 ImageSource 属性,那么无论脏区多大,CPU 占用都是 0。

脏区大小与 CPU 占用率之间的关系

从前面的测试中我们可以发现,脏区的大小在 WriteableBitmap 的渲染里占了绝对的耗时。因此,我把脏区大小与 CPU 占用率之间的关系用图表的形式贴出来,这样可以直观地理解其性能差异。

需要注意,CPU 占用率与机器性能强相关,因此其绝对占用没有意义,但相对大小则有参考价值。

脏区大小 CPU 占用率 帧率
0*0 0.0% 60
1*1 5.1% 60
16*9 5.7% 60
160*90 6.0% 60
320*180 6.5% 60
640*360 6.9% 60
1280*720 7.5% 60
1920*1080 10.5% 60
2560*1440 12.3% 60
3840*2160 16.1% 60

根据这张表我么可以得出:

  • 脏区渲染是 CPU 占用的最大瓶颈(因为没有脏区仅剩内存拷贝的时候 CPU 占用为 0%)

但是有一个需要注意的信息是——虽然 CPU 占用率受脏区影响非常大,但主线程却几乎没有消耗 CPU 占用。此占用基本上全是渲染线程的事。

如果我们分析主线程的性能分布,可以发现内存拷贝现在是性能瓶颈:

后面我们会提到 WriteableBitmap 的渲染原理,也会说到这一点。

启用基准测试(Benchmark)

不过,由于内存数据的拷贝和脏区渲染实际上可以分开到两个不同的线程,如果这两者不同步执行(可能执行次数还有差异)的情况下,内存拷贝也可能成为性能瓶颈的一部分。

于是我将不同的内存拷贝方法进行一个基准测试,便于大家评估使用哪种方法来为 WriteableBitmap 提供渲染数据。

使用 CopyMemory 拷贝内存

++  [Benchmark(Description = "CopyMemory")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void CopyMemory(int width, int height)
    {
        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          CopyMemory(_bitmap.BackBuffer, new IntPtr(ptr), (uint)_empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
[DllImport("kernel32.dll")]
private static extern void CopyMemory(IntPtr destination, IntPtr source, uint length);


  • 1
  • 2

使用 MoveMemory 移动内存

++  [Benchmark(Description = "RtlMoveMemory")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void RtlMoveMemory(int width, int height)
    {
        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          MoveMemory(_bitmap.BackBuffer, new IntPtr(ptr), (uint)_empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
    [DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory")]
    private static extern void MoveMemory(IntPtr dest, IntPtr src, uint count);


  • 1
  • 2

使用 Buffer.MemoryCopy 拷贝内存

需要注意,Buffer.MemoryCopy 是 .NET Framework 4.6 才引入的 API,在 .NET Framework 后续版本以及 .NET Core 的所有版本才可以使用,更旧版本的 .NET Framework 没有这个 API。

++  [Benchmark(Baseline = true, Description = "Buffer.MemoryCopy")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void BufferMemoryCopy(int width, int height)
    {
        _bitmap.Lock();

++      fixed (byte* ptr = _empty4KBitmapArray)
++      {
++          var p = new IntPtr(ptr);
++          Buffer.MemoryCopy(ptr, _bitmap.BackBuffer.ToPointer(), _empty4KBitmapArray.Length, _empty4KBitmapArray.Length);
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

自己写 for 循环

++  [Benchmark(Description = "for for")]
++  [Arguments(3840, 2160)]
++  [Arguments(100, 100)]
    public unsafe void ForForCopy(int width, int height)
    {
        _bitmap.Lock();

++      var buffer = (byte*)_bitmap.BackBuffer.ToPointer();
++      for (var j = 0; j < height; j++)
++      {
++          for (var i = 0; i < width; i++)
++          {
++              var pixel = buffer + j * width * 4 + i * 4;
++              *pixel = 0xff;
++              *(pixel + 1) = 0x7f;
++              *(pixel + 2) = 0x00;
++              *(pixel + 3) = 0xff;
++          }
++      }

        _bitmap.AddDirtyRect(new Int32Rect(0, 0, width, height));
        _bitmap.Unlock();
    }


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

基准测试数据

我们跑一次基准测试:

Method Mean Error StdDev Median Ratio RatioSD
CopyMemory 2.723 ms 0.0642 ms 0.1881 ms 2.677 ms 0.84 0.08
RtlMoveMemory 2.659 ms 0.0740 ms 0.2158 ms 2.633 ms 0.82 0.08
Buffer.MemoryCopy 3.246 ms 0.0776 ms 0.2250 ms 3.200 ms 1.00 0.00
‘for for’ 10.401 ms 0.1979 ms 0.4964 ms 10.396 ms 3.21 0.25
‘CopyMemory with 100*100 dirty region’ 2.446 ms 0.0757 ms 0.2207 ms 2.368 ms 0.76 0.09
‘RtlMoveMemory with 100*100 dirty region’ 2.415 ms 0.0733 ms 0.2161 ms 2.369 ms 0.75 0.08
‘Buffer.MemoryCopy with 100*100 dirty region’ 3.076 ms 0.0612 ms 0.1523 ms 3.072 ms 0.95 0.08
‘for for with 100*100 dirty region’ 10.014 ms 0.2398 ms 0.6995 ms 9.887 ms 3.10 0.29

可以发现:

  1. CopyMemoryRtMoveMemory 性能是最好的,其性能差不多;
  2. 自己写循环拷贝内存的性能是最差的;
  3. 如果 WriteableBitmap 不渲染,那么无论设置多大的脏区都不会对性能有任何影响。

结论和使用建议

综合前面两者的结论,我们可以发现:

  1. WriteableBitmap 的性能瓶颈源于对脏区的重新渲染

    • 脏区为 0 或者不在可视化树渲染,则不消耗性能
    • 只要有脏区,渲染过程就会开始成为性能瓶颈
      • CPU 占用基础值就很高了
      • 脏区越大,CPU 占用越高,但增幅不大
  2. 内存拷贝不是 WriteableBitmap 的性能瓶颈
    • 建议使用 Windows API 或者 .NET API 来拷贝内存(而不是自己写)

另外,如果你有一些特殊的应用场景,可以适当调整下自己写代码的策略:

  • 如果你希望有较大脏区的情况下降低 CPU 占用,可以考虑降低 WriteableBitmap 脏区的刷新率
  • 如果你希望 WriteableBitmap 有较低的渲染延迟,则考虑减小脏区

WriteableBitmap 渲染原理

在调用 WriteableBitmap 的 AddDirtyRect 方法的时候,实际上是调用 MILSwDoubleBufferedBitmap.AddDirtyRect,这是 WPF 专门为 WriteableBitmap 而提供的非托管代码的双缓冲位图的实现。

在 WriteableBitmap 内部数组修改完毕之后,需要调用 Unlock 来解锁内部缓冲区的访问,这时会提交所有的修改。接下来的渲染都交给了 MediaContext,用来完成双缓冲位图的渲染。

private void SubscribeToCommittingBatch()
{
    // Only subscribe the the CommittingBatch event if we are on-channel.
    if (!_isWaitingForCommit)
    {
        MediaContext mediaContext = MediaContext.From(Dispatcher);
        if (_duceResource.IsOnChannel(mediaContext.Channel))
        {
            mediaContext.CommittingBatch += CommittingBatchHandler;
            _isWaitingForCommit = true;
        }
    }
}


  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在上面的 CommittingBatchHandler 中,将渲染指令发送到了渲染线程。

channel.SendCommand((byte*)&command, sizeof(DUCE.MILCMD_DOUBLEBUFFEREDBITMAP_COPYFORWARD));


  • 1

前面我们通过脏区大小可以得出内存拷贝不是 CPU 占用率的瓶颈,脏区大小才是,不过是渲染线程在占用这 CPU 而不是主线程。但是内存拷贝却成为了主线程的瓶颈(当然前面我们给出了数据,实际上非常小)。所以如果试图分析这么高 CPU 的占用,会发现并不能从主线程上调查得出符合预期的结论(因为即便你完全干掉了内存拷贝,CPU 占用依然是这么高)。



我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

如果在博客看到有任何不懂的内容,欢迎交流。我搭建了 dotnet 职业技术学院 欢迎大家加入。

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名吕毅(包含链接:https://walterlv.blog.csdn.net/),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

原文地址:https://www.cnblogs.com/lonelyxmas/p/12159206.html

时间: 2024-11-07 19:02:47

WPF 高性能位图渲染 WriteableBitmap 及其高性能用法示例的相关文章

jquery jtemplates.js模板渲染引擎的详细用法第一篇

jquery jtemplates.js模板渲染引擎的详细用法第一篇 Author:ching Date:2016-06-29 jTemplates是一个基于JQuery的模板引擎插件,功能强大,有了他你就再不用为使用JS绑定数据时发愁了.后端语言使用php,asp.net,jsp等都不是问题,使用模板渲染可以很大程度上提高程序性能,使用异步获取数据,不用整个页面都回发,好处当然不仅仅是这些. 下载jtemplates,官网的文档写得非常的详细 打开官网:http://jtemplates.tp

Android 颜色渲染(四) BitmapShader位图渲染

版权声明:本文为博主原创文章,未经博主允许不得转载. Android 颜色处理(四) BitmapShader位图渲染 public   BitmapShader(Bitmap bitmap,Shader.TileMode tileX,Shader.TileMode tileY) 调用这个方法来产生一个画有一个位图的渲染器(Shader). bitmap   在渲染器内使用的位图 tileX      The tiling mode for x to draw the bitmap in.  

jquery jtemplates.js模板渲染引擎的详细用法第二篇

jquery jtemplates.js模板渲染引擎的详细用法第二篇 关于jtemplates.js的用法在第一篇中已经讲过了,这里就直接上代码,不同之处是绑定模板的方式,这里讲模板的数据专门写一个template.html的文件来展示 <span style="font-family:Microsoft YaHei;font-size:14px;"><!doctype html> <html lang="zh-CN"> <

jquery jtemplates.js模板渲染引擎的详细用法第三篇

jquery jtemplates.js模板渲染引擎的详细用法第三篇 <span style="font-family:Microsoft YaHei;font-size:14px;"><!doctype html> <html lang="zh-CN"> <head> <meta http-equiv="Content-Type" content="text/html; chars

WPF的ListView控件自定义布局用法实例

本文实例讲述了WPF的ListView控件自定义布局用法.分享给大家供大家参考,具体如下: 概要: 以源码的形式贴出,免得忘记后,再到网上查资料.在VS2008+SP1环境下调试通过 引用的GrayscaleEffect模块,可根据参考资料<Grayscale Effect...>中的位置下载. 正文: 如何布局是在App.xaml中定义源码如下 ? 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

openat与open的区别及用法示例

从2.6.16版本开始,GNU/Linux引入opeant系统调用: #define _XOPEN_SOURCE 700 /* Or define _POSIX_C_SOURCE >= 200809 */ #include <fcntl.h> int openat(int dirfd , const char * pathname , int flags , ... /* mode_t mode */); Returns file descriptor on success, or –1

wxpython布局管理部件wx.gridbagsizer用法示例

text = ("This is text box")         panel = wx.Panel(self, -1)         chkAll1 = wx.CheckBox(panel, ID_CHKBOX_CAN_SEL_ALL, u'全选')                chkKnown = wx.CheckBox(panel, ID_CHKBOX_CAN_UNKNOWN, u'不会')         chkUnknow = wx.CheckBox(panel, I

C#中HashTable的用法示例2

命名空间 System.Collections 名称 哈希表(Hashtable) 描述 用于处理和表现类似keyvalue的键值对,其中key通常可用来快速查找,同时key是区分大小写:value用于存储对应于key的值.Hashtable中keyvalue键值对均为object类型,所以Hashtable可以支持任何类型的keyvalue键值对. 二,哈希表的简单操作 Hashtable hshTable = new Hashtable(); //  创建哈希表hshTable .Add("

Linux中 find 常见用法示例

Linux中find常见用法示例 #find path -option [ -print ] [ -exec -ok command ] {} \; #-print 将查找到的文件输出到标准输出 #-exec command {} \; —–将查到的文件执行command操作,{} 和 \;之间有空格.其实在命令执行的时候"{}"将被find到的结果替换掉,因此将"{}"看成find到的文件来进行操作就很容易理解这个选项了. #-ok 和-exec相同,只不过在操作