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.在白纸上完全凭感觉,想象来画图难度很大, ...
随机推荐
- 可视化学习:WebGL实现缩放平移
前言 在上篇文章中,我们使用WebGL实现了网格背景,当时有提到说使用WebGL来实现的好处之一,是网格背景可以与画布上的其他元素更好地融合,比如一起缩放平移,那么在WebGL中怎么实现缩放和平移呢? ...
- 你是怎么理解ES6中 Decorator 的?使用场景?
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助 一.介绍 Decorator,即装饰器,从名字上很容易让我们联想到装饰者模式 简单来讲,装饰者模式就是一种在不改变原类和使用继承的情况下, ...
- MySQL函数GROUP_CONCAT()函数简介
一.数据需求按id分组然后把name用英文逗号分隔开 id name countryid age 1 曹操 1 56 2 刘备 2 47 3 孙权 3 38 4 司马懿 1 61 5 诸葛亮 2 42 ...
- 高防dns和高防IP一样吗?
高防DNS和高防IP一样吗? 高防DNS和高防IP在功能和目标上有所不同,因此它们并不完全相同. 高防DNS是一种针对DNS服务的防护措施,旨在保护域名解析免受DDoS攻击等网络威胁的影响.它利用高防 ...
- C# 人脸检测 人脸比对 活体检测 口罩检测 年龄预测 性别预测 眼睛状态检测
基于以下开源软件做了一个Demo GitHub - ViewFaceCore/ViewFaceCore: C# 超简单的离线人脸识别库.( 基于 SeetaFace6 ) 效果 代码 using Sy ...
- 如何修改 ARM Linux 系统的启动画面
[如何修改 ARM Linux 系统的启动画面 Table of Contents U-Boot 的启动画面 内核的启动画面 PPM 图片格式 显示启动画面的过程 更换启动画面 动态修改启动画面 一些 ...
- 【Java】abstract class 和 interface 有什么区别?
含有 abstract 修饰符的 class 即为抽象类,abstract 类不能创建的实例对象.含有 abstract 方法的类必须定义为 abstract class,abstract class ...
- 汇编语言-int指令
int 指令 int 指令的格式为:int n,n为中断类型码,它的功能是引发终端过程. CPU执行int n指令,相当于引发一个n号中断的中断过程,执行过程如下. 取中断类型码n: 标志寄存器入栈, ...
- Python面试必备一之迭代器、生成器、浅拷贝、深拷贝
本文首发于公众号:Hunter后端 原文链接:Python面试必备一之迭代器.生成器.浅拷贝.深拷贝 这一篇笔记主要介绍 Python 面试过程中常被问到的一些问题,比如: Python 中的迭代器和 ...
- 如何在 Python 中执行 MySQL 结果限制和分页查询
Python MySQL 限制结果 限制结果数量 示例 1: 获取您自己的 Python 服务器 选择 "customers" 表中的前 5 条记录: import mysql.c ...