在 WPF 框架提供方便进行像素读写的 WriteableBitmap 类,本文来告诉大家在咱写下像素到 WriteableBitmap 渲染,底层的逻辑

之前我使用 WriteableBitmap 进行 CPU 高性能绘图时,在性能调试遇到一个问题,写入到 WriteableBitmap 的像素会经过两次拷贝。其中一次是我自己拷贝到 WriteableBitmap 而另一次拷贝就在 WriteableBitmap 里面。无论设置 WriteableBitmap 的脏区多大,渲染的时候是整个图片渲染 。本来按照我的阅读顺序,当前还没有阅读到 WriteableBitmap 的代码,但是有小伙伴和我报告了 WriteableBitmap 的坑,因此我就开始阅读 WriteableBitmap 详细请看 dotnet 读 WPF 源代码笔记 了解 WPF 已知问题 后台线程创建 WriteableBitmap 锁住主线程

在开始之前,先聊聊 WriteableBitmap 是什么?在 WPF 和 UWP 中提供的 WriteableBitmap 是支持对像素写入而更改渲染的图片,当然,本文只聊 WPF 的源代码,关于 UWP 部分,咱只知道使用就可以。通过 WriteableBitmap 可以用来实现高性能的 CPU 渲染,以下是我的其他 WriteableBitmap 博客

  • WPF 使用 Skia 绘制 WriteableBitmap 图片
  • WPF 如何在 WriteableBitmap 写文字
  • WPF 使用不安全代码快速从数组转 WriteableBitmap

在 WriteableBitmap 进行绘制时,有一个重要的功能是设置 DirtyRect 来告诉 WPF 层,当前需要更新的是 WriteableBitmap 的哪个内容。在调试时,可以看到如果 DirtyRect 很小,那么 CPU 占用也将会很小,但渲染时依然是渲染整个图片。在聊到 WriteableBitmap 的渲染和更新,就一定需要先聊到 AddDirtyRect 方法,下面咱看一下 AddDirtyRect 方法的实现

        public void AddDirtyRect(Int32Rect dirtyRect){WritePreamble();if (_lockCount == 0){throw new InvalidOperationException(SR.Get(SRID.Image_MustBeLocked));}//// Sanitize the dirty rect.//dirtyRect.ValidateForDirtyRect("dirtyRect", _pixelWidth, _pixelHeight);if (dirtyRect.HasArea){MILSwDoubleBufferedBitmap.AddDirtyRect(_pDoubleBufferedBitmap,ref dirtyRect);_hasDirtyRects = true;}// Note: we do not call WritePostscript because we do not want to// raise change notifications until the writeable bitmap is unlocked.}

调用 AddDirtyRect 基本都会在 Lock 和 Unlock 方法里面,但无论是 Lock 还是 Unlock 和渲染触发其实都没有关系,咱继续回到 AddDirtyRect 方法。在这个方法里面实际的调用就是 MILSwDoubleBufferedBitmap.AddDirtyRect 方法,这是一个从 MIL 层拿到的方法

        [DllImport(DllImport.MilCore, EntryPoint = "MILSwDoubleBufferedBitmapAddDirtyRect", PreserveSig = false)]internal static extern void AddDirtyRect(SafeMILHandle /* CSwDoubleBufferedBitmap */ THIS_PTR,ref Int32Rect dirtyRect);

从上面的注释可以看到,这里的 SafeMILHandle 的 THIS_PTR 就是 CSwDoubleBufferedBitmap 类型,这个类型定义在 MIL 层,代码在 src\Microsoft.DotNet.Wpf\src\WpfGfx\core\sw\swlib\doublebufferedbitmap.cpp 文件。通过上面代码可以看到,就是定义在字段的 _pDoubleBufferedBitmap 字段

        private SafeMILHandle _pDoubleBufferedBitmap;   // CSwDoubleBufferedBitmap

先忽略 _pDoubleBufferedBitmap 的创建,咱进入 MILSwDoubleBufferedBitmapAddDirtyRect 方法的实现。这是定义在 exports.cpp 的方法

HRESULT
MILSwDoubleBufferedBitmapAddDirtyRect(__in CSwDoubleBufferedBitmap * THIS_PTR,__in const MILRect *pRect)
{HRESULT hr = S_OK;UINT x = 0;UINT y = 0;UINT width = 0;UINT height = 0;CMilRectU rcDirty;CHECKPTR(THIS_PTR);CHECKPTR(pRect);IFC(IntToUInt(pRect->X, &x));IFC(IntToUInt(pRect->Y, &y));IFC(IntToUInt(pRect->Width, &width));IFC(IntToUInt(pRect->Height, &height));// Since we converted x, y, width, and height from ints, we can add them// together and remain within a UINT.rcDirty = CMilRectU(x, y, width, height, XYWH_Parameters);IFC(THIS_PTR->AddDirtyRect(&rcDirty));Cleanup:RRETURN(hr);
}

这里的逻辑是在 MIL 层了,这一层就是实际处理多媒体的逻辑,可以看到上面代码核心的方法就是 THIS_PTR->AddDirtyRect(&rcDirty) 调用 CSwDoubleBufferedBitmap 的 AddDirtyRect 方法。在 AddDirtyRect 方法里面实际上就是维护一个去掉重复范围的 Rect 列表而已,只是因为用了 C++ 编写,代码看起来有点杂

HRESULT
CSwDoubleBufferedBitmap::AddDirtyRect(__in const CMilRectU *prcDirty)
{HRESULT hr = S_OK;CMilRectU rcBounds(0, 0, m_width, m_height, XYWH_Parameters);CMilRectU rcDirty = *prcDirty;if (!rcDirty.IsEmpty()){// Each dirty rect will eventually be treated as a RECT, so we must// ensure that the Left, Right, Top, and Bottom values never exceed// INT_MAX.  We already restrict our dimensions to INT_MAX, so as// long as the dirty rect is fully within the bounds of the bitmap,// we are safe.if (!rcBounds.DoesContain(rcDirty)){IFC(E_INVALIDARG);}// Adding a dirty rect that spans the entire bitmap will simply// replace all existing dirty rects.if (rcDirty.IsEquivalentTo(rcBounds)){m_pDirtyRects[0] = rcBounds;m_numDirtyRects = 1;}else{// Check to see if one of the existing dirty rects fully contains the// new dirty rect.  If so, there is no need to add it.for (UINT i = 0; i < m_numDirtyRects; i++){if (m_pDirtyRects[i].DoesContain(rcDirty)){// No dirty list change - new dirty rect is already included.goto Cleanup;}}// Collapse existing dirty rects if we're about to exceed our maximum.if (m_numDirtyRects >= c_maxBitmapDirtyListSize){// Collapse dirty list to a single large rect (including new rect)while (m_numDirtyRects > 1){m_pDirtyRects[0].Union(m_pDirtyRects[--m_numDirtyRects]);}m_pDirtyRects[0].Union(rcDirty);Assert(m_numDirtyRects == 1);}else{m_pDirtyRects[m_numDirtyRects++] = rcDirty;}}}Cleanup:RRETURN(hr);
}

上面代码是将传入的参数,合入到 m_pDirtyRects 字段里面

可以看到在调用咱的 AddDirtyRect 方法时,其实就是更新 CSwDoubleBufferedBitmap 的 m_pDirtyRects 字段而已,而此时依然没有做渲染相关逻辑。从 CSwDoubleBufferedBitmap 这个命名可以看到,这是双缓存的做法。两个缓存,前面的缓存是用在实际显示的对象,后面的缓存是用的是一个数组用于给 WPF 上层使用访问

在 WPF 的渲染过程中,按照 DirectX 应用的渲染步骤,第一步就是收集过程,在收集过程中收集绘制信息。收集过程中将会调用到 CSwDoubleBufferedBitmap 的 CopyForwardDirtyRects 方法,这个方法的作用就是根据脏区从后面的缓存将像素复制到前面的缓存。虽然这个类的命名是双缓存,但实际上的做法不是在渲染的时候交换两个缓存的指针,而是在渲染收集过程中,从后面的缓存拷贝数据到前面的缓存

以下是 CopyForwardDirtyRects 方法的代码,我在代码里面添加了一些注释

HRESULT
CSwDoubleBufferedBitmap::CopyForwardDirtyRects()
{HRESULT hr = S_OK;IWGXBitmapSource *pIWGXBitmapSource = NULL;IWGXBitmapLock *pFrontBufferLock = NULL;UINT cbLockStride = 0;UINT cbBufferSize = 0;BYTE *pbSurface = NULL;Assert(m_pBackBuffer);// 根据调用 AddDirtyRect 方法加入的 DirtyRect 获取当前有哪些需要拷贝的像素// This locks only the rect specified as dirty for each copy. It would// be more efficient to just lock the entire rect once for all of the// copies, but then we need to manually compute offsets into the front// buffer specific to each pixel format.while (m_numDirtyRects > 0){// We have to jump through a few RECT hoops here since// IWGXBitmapSource::Lock/CopyPixels take a WICRect and// IWGXBitmap::AddDirtyRect takes a GDI RECT, neither of which are// CMilRectU which we use in CSwDoubleBufferedBitmap for geometric operations.//// CMilRectU and RECT share the same memory alignment, but different// signs.  Since we restrict the size of our bitmap to MAX_INT, we can// safely cast.// 这里只是做一层转换而已,拿到当前的一个 DirtyRect 范围const RECT *rcDirty = reinterpret_cast<RECT const *>(&m_pDirtyRects[--m_numDirtyRects]);WICRect copyRegion = {static_cast<int>(rcDirty->left),static_cast<int>(rcDirty->top),static_cast<int>(rcDirty->right - rcDirty->left),static_cast<int>(rcDirty->bottom - rcDirty->top)};// 根据 IWICBitmapSource 的使用文档,在使用之前需要先加上锁// This adds copyRegion as a dirty rect to m_pFrontBuffer automatically.IFC(m_pFrontBuffer->Lock(&copyRegion,MilBitmapLock::Write,&pFrontBufferLock));IFC(pFrontBufferLock->GetStride(&cbLockStride));IFC(pFrontBufferLock->GetDataPointer(&cbBufferSize, &pbSurface));// If a format converter has been allocated, it is necessary that we call copy// pixels through it rather than directly from the back buffer since its very// existence implies that a conversion is needed.GetPossiblyFormatConvertedBackBuffer(&pIWGXBitmapSource);// 这里的 IFC 是一个宏,表示的是如果返回值是 gg 的,那么 goto 到 Cleanup 标签/** #ifndef IFC#define IFC(x) { hr = (x); if (FAILED(hr)) goto Cleanup; }#endif*/// 下面代码就是核心逻辑,通过 CopyPixels 方法从后面的缓存也就是 WPF 层的数据拷贝到前面的缓存用于显示// 在这一层里面其实就丢失了 DirtyRect 信息IFC(pIWGXBitmapSource->CopyPixels(&copyRegion,cbLockStride,cbBufferSize,pbSurface));// 释放掉锁// We need to release the lock and format converter here because we are in a loop.ReleaseInterface(pIWGXBitmapSource);ReleaseInterface(pFrontBufferLock);}Cleanup:ReleaseInterfaceNoNULL(pIWGXBitmapSource);ReleaseInterfaceNoNULL(pFrontBufferLock);RRETURN(hr);
}

从上面代码可以看到,咱在使用 WriteableBitmap 的两次复制的第二次复制就是上面的代码,通过 pIWGXBitmapSource->CopyPixels 的过程就会依赖传入的 DirtyRect 决定拷贝的数据量。也就是说通过 DirtyRect 能优化的性能也只是更新前面的缓存用到的拷贝的性能,我没有在官方文档里面找到 CopyPixels 里面还会记录 DirtyRect 的功能,同时也没有在 WPF 自定义渲染管线里面找到只刷新图片某个范围的逻辑,因此可以认为使用 WriteableBitmap 的更新,设置 DirtyRect 只影响第二次复制数据的性能,而不会影响渲染性能,依然是整个图片进行渲染

在拷贝到前面的缓存之后,在 WPF 中是在自定义渲染管线里面将前面的缓存作为纹理绘制到形状上,在 WPF 上,可以将 WriteableBitmap 作为 BitmapSource 放入到不规则形状上,将图片作为纹理绘制到形状上能做到比较通用。关于 WPF 的从图片到渲染的步骤,就需要额外的文档来告诉大家

当前的 WPF 在 https://github.com/dotnet/wpf 完全开源,使用友好的 MIT 协议,意味着允许任何人任何组织和企业任意处置,包括使用,复制,修改,合并,发表,分发,再授权,或者销售。在仓库里面包含了完全的构建逻辑,只需要本地的网络足够好(因为需要下载一堆构建工具),即可进行本地构建

详细请看 IWICBitmapSource::CopyPixels (wincodec.h) - Win32 apps

我搭建了自己的博客 https://blog.lindexi.com/ 欢迎大家访问,里面有很多新的博客。只有在我看到博客写成熟之后才会放在csdn或博客园,但是一旦发布了就不再更新

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

如有不方便在博客评论的问题,可以加我 QQ 2844808902 交流


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

dotnet 读 WPF 源代码笔记 WriteableBitmap 的渲染和更新是如何实现相关推荐

  1. dotnet 读 WPF 源代码笔记 AppDomainShutdownMonitor 的设计

    本文是我在读 WPF 源代码做的笔记.在 WPF 中的 AppDomainShutdownMonitor 类是一个不开放的类,这个类当前只是给 D3DImage 类使用.在 AppDomainShut ...

  2. dotnet 读 WPF 源代码笔记 渲染收集是如何触发

    在 WPF 里面,渲染可以从架构上划分为两层.上层是 WPF 框架的 OnRender 之类的函数,作用是收集应用程序渲染的命令.上层将收集到的应用程序绘制渲染的命令传给下层,下层是 WPF 的 GF ...

  3. dotnet 读 WPF 源代码笔记 创建 SolidColorBrush 性能没有想象那么差

    在 WPF 中,常用的画刷里面有纯色画刷 SolidColorBrush 类.因为画刷会对应到 DirectX 的资源,因此之前我以为纯色画刷其实会比 Color 会占用更多的资源.在 WPF 中 C ...

  4. dotnet 读 WPF 源代码笔记 了解 WPF 已知问题 用户设备上不存在 Arial 字体将导致应用闪退...

    本文来告诉大家 WPF 已知问题,在用户的设备上,如果不存在 Arial 字体,同时安装了一些诡异的字体,那么也许就会让应用在使用到诡异的字体的时候,软件闪退 在 WPF 的 FontFamily.c ...

  5. dotnet 读 WPF 源代码笔记 提升调试效率的 NamedObject 类型

    本文来聊聊 WPF 那些值得称赞的设计中的 NamedObject 类型.在 WPF 中,有很多值得我学习的设计开发思想,其中就包括本文将要介绍的 NamedObject 类型.此类型的定义仅仅只是为 ...

  6. dotnet 读 WPF 源代码笔记 插入触摸设备的初始化获取设备信息

    在 WPF 触摸应用中,插入触摸设备,即可在应用里面使用上插入的触摸设备.在 WPF 使用触摸设备的触摸时,需要获取到触摸设备的信息,才能实现触摸 获取触摸设备插入 在 WPF 中,通过 Window ...

  7. 《深入浅出WPF》笔记——事件篇

    如果对事件一点都不了解或者是模棱两可的话,建议先去看张子阳的委托与事件的文章(比较长,或许看完了,也忘记看这一篇了,没事,我会原谅你的)http://www.cnblogs.com/JimmyZhan ...

  8. 在Linux上编译dotnet cli的源代码生成.NET Core SDK的安装包

    .NET 的开源,有了更多的DIY乐趣.这篇博文记录一下在新安装的 Linux Ubuntu 14.04 上通过自己动手编译 dotnet cli 的源代码生成 .net core sdk 的 deb ...

  9. 《深入浅出WPF》笔记——绑定篇(一)

    上一节,有记录写到:在WPF里,数据驱动UI,数据占核心地位,UI次之.怎么恢复数据的核心地位,那就要先了解一下Binding. 一.Binding 基础 1.1WPF中Data Binding的带来 ...

最新文章

  1. java i 原子_为什么i ++不是原子的?
  2. 推荐一个非常火爆的电商开源项目!
  3. 冷藏温度范围_冰箱冷藏温度多少合适 冰箱冷藏温度调节范围
  4. ReentrantLock 分析
  5. SQL Server Alwayson架构下 服务器 各虚拟IP漂移监控告警的功能实现 -1(服务器视角)...
  6. 乔布斯《我生命中的三个故事》
  7. 问题解决:pip无法使用,经升级后可以使用
  8. c语言大作业背单词,c语言必背代码 c语言入门必背单词 c语言必背100代码
  9. 【Vue】实现出生日期计算年龄
  10. 转载:技术大停滞——范式春梦中的地球工业文明5:台阶前的坑:人类社会的宿命
  11. 20几岁要懂点经济学【笔记】
  12. 《K8s与云原生应用》之K8s的系统架构与设计理念
  13. Guava1.0—— 目录
  14. [计算几何] (二维)圆与直线的交点
  15. ⽤户去输⼊⼀个⼈民币⾦额,然后程序会计算如何去⽤20元纸币,10元纸币,5元纸币和⼀元纸币去表⽰这个⼈民币⾦额。要求使⽤最⼩数⽬的纸币。
  16. 《自控力》第八章读书笔记
  17. 2022危险化学品经营单位安全管理人员考试题库模拟考试平台操作
  18. 关于亲现实儿童游戏系统的解释
  19. echart 柱状图倒叙、字体设置、颜色渐变的使用
  20. 忙里偷闲( ˇˍˇ )闲里偷学【C语言篇】——(5)有趣的指针

热门文章

  1. 中文语音克隆|MockingBird(拟声鸟)github项目运行流程(一次跑通)
  2. 关于css盒子模型和BFC的理解
  3. 高性能计算在石油物探中的应用现状与前景
  4. 第一坊颜韵YCY恶心主播,忘恩负义白眼狼说颜韵真不过分
  5. python无法运行图像_OpenCV Python不使用imread()打开图像
  6. 我为什么选择博客园!
  7. 程序猿专用代码注释:佛祖保佑,永无BUG
  8. 烟气脱硫技术及工艺流程超全剖析
  9. Day73.SpringMVC案例:影院系统、使用Restful风格重构
  10. 微型计算机cpu主要有两部分构成 他们是,【单选题】微型计算机的CPU主要由两部分构成,它们是( ) A. 内存和控制器 B. 内存和外存 C. 运算器和控制器 D. 外存和运算器...