在 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(
©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
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/285412.html
標籤:WPF
下一篇:WPF實作頭像裁剪
