在 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 博客

在 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

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

  1. dotnet 读 WPF 源代码笔记 布局时 Arrange 如何影响元素渲染坐标

    大家是否好奇,在 WPF 里面,对 UIElement 重写 OnRender 方法进行渲染的内容,是如何受到上层容器控件的布局而进行坐标偏移.如有两个放入到 StackPanel 的自定义 UIEl ...

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

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

  3. 《深入浅出WPF》笔记——绘画与动画

    <深入浅出WPF>笔记——绘画与动画   本篇将记录一下如何在WPF中绘画和设计动画,这方面一直都不是VS的强项,然而它有一套利器Blend:这方面也不是我的优势,幸好我有博客园,能记录一 ...

  4. 读Flask源代码学习Python--config原理

    读Flask源代码学习Python--config原理 个人学习笔记,水平有限.如果理解错误的地方,请大家指出来,谢谢!第一次写文章,发现好累--!. 起因   莫名其妙在第一份工作中使用了从来没有接 ...

  5. 《深入浅出WPF》笔记——资源篇

    原文:<深入浅出WPF>笔记--资源篇 前面的记录有的地方已经用到了资源,本文就来详细的记录一下WPF中的资源.我们平时的“资源”一词是指“资财之源”,是创造人类社会财富的源泉.在计算机程 ...

  6. DirectX11笔记(六)--Direct3D渲染2--VERTEX BUFFER

    原文:DirectX11笔记(六)--Direct3D渲染2--VERTEX BUFFER 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/u0103 ...

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

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

  8. [WPF源代码]QQ空间相册下载工具

    放一个WPF源代码,源代码地址 http://download.csdn.net/detail/witch_soya/6195987 代码没多少技术含量,就是用WPF做的一个QQ空间相册下载工具,效果 ...

  9. SDL2源代码分析3:渲染器(SDL_Renderer)

    ===================================================== SDL源代码分析系列文章列表: SDL2源代码分析1:初始化(SDL_Init()) SDL ...

  10. WPF学习笔记-用Expression Design制作矢量图然后导出为XAML

    WPF学习笔记-用Expression Design制作矢量图然后导出为XAML 第一次用Windows live writer写东西,感觉不错,哈哈~~ 1.在白纸上完全凭感觉,想象来画图难度很大, ...

随机推荐

  1. 记录--分享8个非常实用的Vue自定义指令

    这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 在 Vue,除了核心功能默认内置的指令 ( v-model 和 v-show ),Vue 也允许注册自定义指令.它的作用价值在于当开发人员 ...

  2. #阶梯NIM,树形dp#CF1498F Christmas Game

    题目 Alice 和 Bob 在一棵 \(n\) 个点的树上玩游戏,第 \(i\) 个节点上有 \(a_i\) 个石子, 每轮可以选择一个深度至少为 \(k\) 的节点并移动任意多石子到其 \(k\) ...

  3. Docker 学习路线 3:安装设置 Docker Desktop 与 Docker 引擎指南

    Docker提供了一个名为Docker Desktop的桌面应用程序,简化了安装和设置过程.还有另一个选项可以使用Docker引擎进行安装. Docker Desktop网站 Docker引擎 Doc ...

  4. C#_面试题1

    C#_面试题1 1.维护数据库的完整性.一致性.你喜欢用触发器还是自写业务逻辑?为什么? 答:尽可能用约束(包括CHECK.主键.唯一键.外键.非空字段)实现,这种方式的效率最好:其次用触发器,这种方 ...

  5. Linux 编译 libjpeg-9e

    jpeg的库有两个:一个是官方的 libjpeg  还有一个是 libjpeg-turbo JPEG库(libjpeg-turbo):https://libjpeg-turbo.org/ Libjpe ...

  6. Python读写json文件--json

    import json # 将数据写入json文件 def json_write_file(): data={'name':'张三','age':12} with open('json.json',' ...

  7. .NET MAUI开源免费的UI工具包 - Uranium

    前言 一直有小伙伴在微信公众号后台留言让我分享一下.NET MAUI相关的UI框架,今天大姚分享一个.NET MAUI开源.免费的UI工具包:Uranium. Uranium介绍 Uranium是一个 ...

  8. HarmonyOS 管理页面跳转及浏览记录导航

      历史记录导航 使用者在前端页面点击网页中的链接时,Web组件默认会自动打开并加载目标网址.当前端页面替换为新的加载链接时,会自动记录已经访问的网页地址.可以通过forward()和backward ...

  9. 最后一站qsnctfwp

    题目附件 图片一: 图片二: 根据图片一判断出位置为南昌市,地铁线路为4号线 根据题目名判断出搜索范围为白马山站或鱼尾洲站 通过百度地图全景地图查看两站环境,发现白马山站以工业区为主,鱼尾洲站以住宅区 ...

  10. CentOS 6.3挂载读写NTFS分区(ntfs-3g) [亲测成功]

    CentOS 6.3挂载读写NTFS分区(ntfs-3g) CentOS不像Fedora,默认是没有自动挂载NTFS的,而它可以利用NTFS-3G来实现挂载及读写. NTFS-3G 是一个开源的软件, ...