WPF 从 WriteableBitmap 里获取到渲染线程使用的 IWICBitmap 对象 ...

打印 上一主题 下一主题

主题 1717|帖子 1717|积分 5151

在 WPF 框架底层内里,对 WriteableBitmap 的实现是有双份内存,具体实现放在 CSwDoubleBufferedBitmap 内里。表层的内存是一个数组,里层内存是 IWICBitmap 对象,渲染时将使用 IWICBitmap 对象到场到渲染管线
本文将告诉大家如何在 WPF 内里,从 WriteableBitmap 里获取到渲染线程使用的 IWICBitmap 对象。本文提供的方法仅仅只能用于辅助大家相识 WPF 的机制,对实际产物使用没有资助。无法通过本文提供的方式减少 WriteableBitmap 的一次 CPU 拷贝
dotnet 读 WPF 源代码笔记 WriteableBitmap 的渲染和更新是如何实现 博客所述,在 WPF 框架底层使用 CSwDoubleBufferedBitmap 存储 WriteableBitmap 的数据。如 CSwDoubleBufferedBitmap 命名所示,这是一个包含了双缓存的类型
在 CSwDoubleBufferedBitmap 内里包含的双缓存可以分为表层和里层两个缓存。表层是一个数组,用于给 WPF 应用上层业务方使用,直接可以在 UI 线程任何时刻使用。里层是 IWICBitmap 对象,这个 IWICBitmap 对象将从表层内存拷贝数据,渲染时将被到场到管线中到场绘制
这个过程内里,将发生两次 CPU 到 CPU 的内存拷贝,以及一次 CPU 到 GPU 的内存拷贝。为什么这么说呢,起首业务方须要将数据写入到 WriteableBitmap 内里,这个过程就是第一次 CPU 到 CPU 的内存拷贝。不严谨的表述可以这么以为,写入到 WriteableBitmap 的过程就是将数据“拷贝”到 WriteableBitmap 的过程,这个过程中完全发生在 CPU 盘算内里,这就是我说的第一次 CPU 到 CPU 的内存拷贝过程。那第二次 CPU 到 CPU 发生在哪?发生在渲染线程中,第一次 CPU 到 CPU 拷贝过程内里是将业务方的数据拷贝到 WriteableBitmap 的表层内存内里,即 CSwDoubleBufferedBitmap 的 m_pFrontBuffer 内里,第二次 CPU 到 CPU 拷贝是在 CSwDoubleBufferedBitmap 内里,在 CSwDoubleBufferedBitmap.CopyForwardDirtyRects 内里从 m_pFrontBuffer 拷贝到 CWriteProtectedBitmap *  m_pBackBufferAsWriteProtectedBitmap 内里,即从表层内存拷贝到里层 IWICBitmap 对象里
最后一次 CPU 到 GPU 的内存拷贝是什么呢?那就是将 IWICBitmap 对象到场到渲染管线时,从 CPU 将数据同步到 GPU 上
我开始以为可以绕过 CSwDoubleBufferedBitmap 内里的一次 CPU 到 CPU 的拷贝,减少表层内存拷贝到里层 IWICBitmap 对象里的耗时,我直接就访问 CSwDoubleBufferedBitmap 的 IWICBitmap * m_pBackBuffer 对象,对其写入数据,于是就举行了本文的测试
当然了,最后我发现即使我成功拿到了 IWICBitmap 对象,且对其写入数据,最后要么就是没有关照到变更没有革新渲染,要么就是被表层内存给覆盖了。没有任何的优化
为了获取到 CSwDoubleBufferedBitmap 的 IWICBitmap * m_pBackBuffer 对象,我按照 lsj 教我的方法,从 VisualStudio 内里查看 CSwDoubleBufferedBitmap 类的布局。即鼠标移动到类定义上,点击查看内存布局,即可看到如下图内容

云云即可看到 IWICBitmap * m_pBackBuffer 对象是 32 偏移量,刚好这是第一个指针,无论是 x86 还是 x64 都是 32 偏移量,于是定义如下布局体
  1. [StructLayout(LayoutKind.Explicit)]
  2. struct CSwDoubleBufferedBitmap
  3. {
  4.     // IWICBitmap *                        m_pBackBuffer;
  5.     [FieldOffset(32)]
  6.     public nint WicBitmap; // 刚好 m_pBackBuffer 就是第一个指针字段,无论是 x86 还是 x64 都刚好是第 32 个字节
  7. }
复制代码
在 WriteableBitmap 类型内里,将其存放到 _pDoubleBufferedBitmap 字段内里,如以下 WPF 代码所示
  1.     public sealed class WriteableBitmap : BitmapSource
  2.     {
  3.         ... // 忽略其他代码
  4.         private SafeMILHandle _pDoubleBufferedBitmap;   // CSwDoubleBufferedBitmap
  5.     }
复制代码
简单使用反射获取一下,然后指针转换为 struct CSwDoubleBufferedBitmap 布局体,代码如下
  1.         var writeableBitmap = (WriteableBitmap) Image.Source;
  2.         var type = writeableBitmap.GetType();
  3.         var fieldInfo = type.GetField("_pDoubleBufferedBitmap", BindingFlags.NonPublic | BindingFlags.Instance);
  4.         if (fieldInfo?.GetValue(writeableBitmap) is SafeHandleZeroOrMinusOneIsInvalid
  5.             doubleBufferedBitmapHandle)
  6.         {
  7.             var handle = doubleBufferedBitmapHandle.DangerousGetHandle();
  8.             var doubleBufferedBitmap = Marshal.PtrToStructure<CSwDoubleBufferedBitmap>(handle);
  9.             ... // 忽略其他代码
  10.         }
复制代码
以上拿到的 doubleBufferedBitmap 就是 struct CSwDoubleBufferedBitmap 布局体
为了测试所拿到的 CSwDoubleBufferedBitmap.WicBitmap 是 IWICBitmap 对象,我通过 stakx.WIC 库将其转换为 stakx.WIC.IWICBitmap 对象,代码如下
  1.             var wicBitmap = (IWICBitmap) Marshal.GetObjectForIUnknown(doubleBufferedBitmap.WicBitmap);
  2.             var size = wicBitmap.GetSize();
复制代码
以上实验调用 GetSize 方法,假如能够返回创建 WriteableBitmap 时传入的尺寸,则证明获取精确
再进一步阅读 WPF 源代码,我发现了着实不须要从 CSwDoubleBufferedBitmap 内里即可拿到 IWICBitmap 对象,这是因为在 WriteableBitmap 还存在如下这一段代码
  1.     public sealed class WriteableBitmap : BitmapSource
  2.     {
  3.         ... // 忽略其他代码
  4.         private bool AcquireBackBuffer(TimeSpan timeout, bool waitForCopy)
  5.         {
  6.                 ... // 忽略其他代码
  7.             MILSwDoubleBufferedBitmap.GetBackBuffer(
  8.                 _pDoubleBufferedBitmap,
  9.                 out _pBackBuffer,
  10.                 out _backBufferSize);
  11.                 _syncObject = WicSourceHandle = _pBackBuffer;
  12.                 ... // 忽略其他代码
  13.         }
  14.         private BitmapSourceSafeMILHandle _pBackBuffer; // IWICBitmap
  15.     }
复制代码
即可以直接拿到 WicSourceHandle 属性或 _pBackBuffer 字段即可,反射获取的代码如下
  1.         var writeableBitmap = (WriteableBitmap) Image.Source;
  2.         var type = writeableBitmap.GetType();
  3.         var propertyInfo = type.GetProperty("WicSourceHandle", BindingFlags.NonPublic | BindingFlags.Instance);
  4.         var value = propertyInfo?.GetValue(writeableBitmap);
  5.         nint wicBitmapHandle = 0;
  6.         if (value is SafeHandleZeroOrMinusOneIsInvalid safeHandle)
  7.         {
  8.             var handle = safeHandle.DangerousGetHandle();
  9.             wicBitmapHandle = handle;
  10.             var wicBitmap = (IWICBitmap) Marshal.GetObjectForIUnknown(handle);
  11.             var size = wicBitmap.GetSize();
  12.             var buffer = new byte[size.Width * size.Height * 4];
  13.             Random.Shared.NextBytes(buffer);
  14.             // 这里的绘制是无效的,因为在 WPF 底层会重新被 m_pFrontBuffer 覆盖
  15.             wicBitmap.CopyPixels(4 * size.Width, buffer);
  16.         }
  17.         Debug.Assert(wicBitmapHandle != 0);
复制代码
为了证明通过 WicSourceHandle 属性获取的和从 CSwDoubleBufferedBitmap 获取到相同的值,我添加了 wicBitmapHandle 局部变量,修改代码添加如下判断逻辑,实际证明两个方式获取到的是相同的值
  1.         var fieldInfo = type.GetField("_pDoubleBufferedBitmap", BindingFlags.NonPublic | BindingFlags.Instance);
  2.         if (fieldInfo?.GetValue(writeableBitmap) is SafeHandleZeroOrMinusOneIsInvalid
  3.             doubleBufferedBitmapHandle)
  4.         {
  5.             var handle = doubleBufferedBitmapHandle.DangerousGetHandle();
  6.             var doubleBufferedBitmap = Marshal.PtrToStructure<CSwDoubleBufferedBitmap>(handle);
  7.             Debug.Assert(doubleBufferedBitmap.WicBitmap == wicBitmapHandle);
  8.         }
复制代码
这也符合 CSwDoubleBufferedBitmap 内里的逻辑
  1. void
  2. CSwDoubleBufferedBitmap::GetBackBuffer(
  3.     __deref_out IWICBitmap **ppBackBuffer,
  4.     __out_opt UINT * pBackBufferSize
  5.     ) const
  6. {
  7.     SetInterface(*ppBackBuffer, m_pBackBuffer);
  8.     if (pBackBufferSize != NULL)
  9.     {
  10.         *pBackBufferSize = m_backBufferSize;
  11.     }
  12. }
复制代码
本文代码放在 githubgitee 上,可以使用如下下令行拉代替码。我整个代码仓库比力巨大,使用以下下令行可以举行部分拉取,拉取速度比力快
先创建一个空文件夹,接着使用下令行 cd 下令进入此空文件夹,在下令行内里输入以下代码,即可获取到本文的代码
  1. git init
  2. git remote add origin https://gitee.com/lindexi/lindexi_gd.git
  3. git pull origin a920291d64e1163ffa40f4134c2a8c56cbbf1342
复制代码
以上使用的是国内的 gitee 的源,假如 gitee 不能访问,请替换为 github 的源。请在下令行继续输入以下代码,将 gitee 源换成 github 源举行拉代替码。假如依然拉取不到代码,可以发邮件向我要代码
  1. git remote remove origin
  2. git remote add origin https://github.com/lindexi/lindexi_gd.git
  3. git pull origin a920291d64e1163ffa40f4134c2a8c56cbbf1342
复制代码
获代替码之后,进入 WPFDemo/CabawgakaicurrecalLalkiniyajagear 文件夹,即可获取到源代码
更多 WPF 框架原理博客,请参阅 博客导航

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
继续阅读请点击广告

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

万有斥力

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表