dotnet 读 WPF 源代码笔记 WriteableBitmap 的渲染和更新是如何实现
在 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(
©Region,
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(
©Region,
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 的渲染和更新是如何实现的更多相关文章
- dotnet 读 WPF 源代码笔记 布局时 Arrange 如何影响元素渲染坐标
大家是否好奇,在 WPF 里面,对 UIElement 重写 OnRender 方法进行渲染的内容,是如何受到上层容器控件的布局而进行坐标偏移.如有两个放入到 StackPanel 的自定义 UIEl ...
- dotnet 读 WPF 源代码笔记 渲染收集是如何触发
在 WPF 里面,渲染可以从架构上划分为两层.上层是 WPF 框架的 OnRender 之类的函数,作用是收集应用程序渲染的命令.上层将收集到的应用程序绘制渲染的命令传给下层,下层是 WPF 的 GF ...
- 《深入浅出WPF》笔记——绘画与动画
<深入浅出WPF>笔记——绘画与动画 本篇将记录一下如何在WPF中绘画和设计动画,这方面一直都不是VS的强项,然而它有一套利器Blend:这方面也不是我的优势,幸好我有博客园,能记录一 ...
- 读Flask源代码学习Python--config原理
读Flask源代码学习Python--config原理 个人学习笔记,水平有限.如果理解错误的地方,请大家指出来,谢谢!第一次写文章,发现好累--!. 起因 莫名其妙在第一份工作中使用了从来没有接 ...
- 《深入浅出WPF》笔记——资源篇
原文:<深入浅出WPF>笔记--资源篇 前面的记录有的地方已经用到了资源,本文就来详细的记录一下WPF中的资源.我们平时的“资源”一词是指“资财之源”,是创造人类社会财富的源泉.在计算机程 ...
- DirectX11笔记(六)--Direct3D渲染2--VERTEX BUFFER
原文:DirectX11笔记(六)--Direct3D渲染2--VERTEX BUFFER 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/u0103 ...
- 在Linux上编译dotnet cli的源代码生成.NET Core SDK的安装包
.NET 的开源,有了更多的DIY乐趣.这篇博文记录一下在新安装的 Linux Ubuntu 14.04 上通过自己动手编译 dotnet cli 的源代码生成 .net core sdk 的 deb ...
- [WPF源代码]QQ空间相册下载工具
放一个WPF源代码,源代码地址 http://download.csdn.net/detail/witch_soya/6195987 代码没多少技术含量,就是用WPF做的一个QQ空间相册下载工具,效果 ...
- SDL2源代码分析3:渲染器(SDL_Renderer)
===================================================== SDL源代码分析系列文章列表: SDL2源代码分析1:初始化(SDL_Init()) SDL ...
- WPF学习笔记-用Expression Design制作矢量图然后导出为XAML
WPF学习笔记-用Expression Design制作矢量图然后导出为XAML 第一次用Windows live writer写东西,感觉不错,哈哈~~ 1.在白纸上完全凭感觉,想象来画图难度很大, ...
随机推荐
- 记录--你敢信?比 setTimeout 还快 80 倍的定时器
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 起因 很多人都知道,setTimeout是有最小延迟时间的,根据MDN 文档 setTimeout:实际延时比设定值更久的原因:最小延迟时 ...
- [Java]小功能
[版权声明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) https://blog.csdn.net/m0_69908381/article/details/130858061 出自[进步* ...
- Error in beforeDestroy hook: “Error: [ElementForm]unpected width “found in
吹水,可忽略 当我尝试吧el-form中labelWidth设为auto时,刷新页面获取到了上面的错误 百思不得其解,我貌似没有在beforeDestroy进行操作,为何会报这个错误 果断各种百度,G ...
- java:寻找两个字符串的最长公共子串
java:寻找两个字符串的最长公共子串 // 找一个字符串的所有子串 public static List<String> findAllStr(String s) { List<S ...
- Scala mutable.Map可变的Map
1 package chapter07 2 3 import scala.collection.mutable 4 5 object Test09_MutableMap { 6 def main(ar ...
- Luna likes Love 题解
Problem Link 简要题意 题目很清楚. 分析 定理 两个人中左边的人一直向右运动,和两人向中间走所用的 步数相同 证明 假设有两组人为 \(a_l , a_r , b_l , b_r (a_ ...
- mybatis踩坑之integer类型是0的时候会被认为0!=''是假
当你的参数类型是integer类型,并且传的是0的时候,在SQL里面做if判断的时候 <if test="auditStatus != null and auditStatus != ...
- #树状数组,欧拉函数#CF594D REQ
题目 给定 \(n\) 个数,求 \(\varphi(\prod_{i=l}^r{a_i})\) 分析 考虑单个欧拉函数的求法,只需要求出这个数的质因数计算即可. 那么考虑离线,枚举右端点,记录每个质 ...
- #最小生成树,Trie#CF888G Xor-MST
题目 给定 \(n\) 个结点的无向完全图.每个点有一个点权为 \(a_i\) . 连接 \(i\) 号结点和 \(j\) 号结点的边的边权为 \(a_i\oplus a_j\) . 求这个图的 MS ...
- #容斥,排列组合#U138404 选数字
题目 给定长度为\(n,n\leq 10^5\)的序列\(a,a_i,m\leq 255\),多组询问求 \[\sum_{i=l}^{r-2}\sum_{j=i+1}^{r-1}\sum_{k=j+1 ...