WPF 从最底层源代码了解 AllowsTransparency 性能差的原因

当前的 WPF 的源代码完全开放,本文将从最底层的 WPF 代码告诉大家为什么设置了 AllowsTransparency 之后性能会变差,以及 WPF 透明的原理

特别感谢 少珺 的研究,我只是将他告诉我的内容写出来,告诉大家

本文将会告诉大家 AllowsTransparency 设置为 true 之后,为什么整体渲染性能降低,将会占用更多 CPU 资源。以及在 4k 下使用更多内存的原因

本文代码基于 WPF 官方开源仓库 所了解,部分逻辑也许和 .NET Framework 不同版本有出入

在 WPF 的实现窗口透明逻辑中,可以在窗口设置 AllowsTransparency = true 让窗口设置透明笔刷的时候,可以看到窗口后面的内容。这个特性由 Windows 的底层 UpdateLayeredWindow 提供或 UpdateLayeredWindowIndirect 提供

在 WPF 的窗口渲染底层的 WPF_GFX 库里面的入口是在 d3ddevice.cpp 的 Present 方法,方法签名如下

HRESULT
CD3DDeviceLevel1::Present(
    __in_ecount(1) CD3DSwapChain const *pD3DSwapChain,
    __in_ecount_opt(1) CMILSurfaceRect const *prcSource,
    __in_ecount_opt(1) CMILSurfaceRect const *prcDest,
    __in_ecount(1) CMILDeviceContext const *pMILDC,
    __in_ecount_opt(1) RGNDATA const * pDirtyRegion,
    DWORD dwD3DPresentFlags
    )

在这个方法里面的核心逻辑是通过 pMILDC->PresentWithHAL() 决定是否走 PresentWithD3D 还是走 PresentWithGDI 方法,如下面代码

    if (pMILDC->PresentWithHAL())
    {
        PresentWithD3D(
            pD3DSwapChain->m_pD3DSwapChain,
            prcSource,
            prcDest,
            pMILDC,
            pDirtyRegion,
            dwD3DPresentFlags,
            &fPresentProcessed
            );
    }
    else
    {
        PresentWithGDI(
            pD3DSwapChain,
            prcSource,
            prcDest,
            pMILDC,
            pDirtyRegion,
            &fPresentProcessed
            );
    }

上面代码中的核心逻辑就是通过 PresentWithD3D 使用 D3D 的方法,或通过 PresentWithGDI 使用 GDI 的方法。有趣的是根据静态代码分析工具人 少珺 的研究,基本都是进入了 PresentWithGDI 方法,也就是实际上进行的是 GDI 渲染。以下代码是 PresentWithGDI 方法签名

HRESULT
CD3DDeviceLevel1::PresentWithGDI(
    __in_ecount(1) CD3DSwapChain const *pD3DSwapChain,
    __in_ecount_opt(1) CMILSurfaceRect const * prcSource,
    __in_ecount_opt(1) CMILSurfaceRect const * prcDest,
    __in_ecount(1) CMILDeviceContext const * pMILDC,
    __in_ecount_opt(1) RGNDATA const * pDirtyRegion,
    __out_ecount(1) bool *pfPresentProcessed
    )

在这个函数里面的核心逻辑如下

    HDC hdcBackBuffer = NULL;

    CD3DSurface *pBackBufferSurface = NULL;
    UINT uBufferWidth;
    UINT uBufferHeight;
    CMilRectU rcSource;

    IFC(pD3DSwapChain->GetBackBuffer(
        0,
        &pBackBufferSurface
        ));

    pBackBufferSurface->GetSurfaceSize(
        &uBufferWidth,
        &uBufferHeight
        );

    // 忽略代码,计算 rcSource 的值

    IFC(pD3DSwapChain->GetDC(
        0,
        rcSource,
        OUT &hdcBackBuffer
        ));

    if ((pMILDC->GetRTInitializationFlags() & MilRTInitialization::PresentUsingMask) == MilRTInitialization::PresentUsingUpdateLayeredWindow)
    {
        SIZE sz = { uBufferWidth, uBufferHeight };
        POINT ptSrc = { 0, 0 };
        HWND hWnd = pMILDC->GetHWND();

        hr = UpdateLayeredWindowEx(
            hWnd,
            NULL, // front buffer
            &pMILDC->GetPosition(),
            &sz,
            hdcBackBuffer,
            &ptSrc,
            pMILDC->GetColorKey(), // colorkey
            &pMILDC->GetBlendFunction(), // blendfunction
            pMILDC->GetULWFlags(), // flags
            prcSource
            );
       
    }

以上代码中的 IFC 只是一个宏而已,还请忽略。通过上面代码,就可以了解到为什么占用内存比较多的一个原因,那就是在内存中重新开辟了一段内存,内存的大小就是窗口的大小。因此可以回答本文的为什么在 4k 下将会占用更多的内存的问题,其实是需要在 4k 下进行全屏的窗口才会占用很多内存,因为在如上代码里面重新申请了一段内存,这个内存大小和窗口大小是关联的

在上面代码中申请的内存的用途是用来从 D3D 拷贝出来,用于后续做 GDI 渲染使用。实际的拷贝逻辑放在 pD3DSwapChain->GetDC 方法里面,通过这个方法获取了 hdcBackBuffer 对象。而这个方法的核心逻辑是放在 d3dswapchainwithswdc.cpp 类里面,代码大概如下

//      Gets a DC that refers to a system memory bitmap.
//
//      The system memory bitmap is updated during this call. The dirty rect is
//      used to determine how much of it needs updating.
//

HRESULT
CD3DSwapChainWithSwDC::GetDC(
    /*__in_range(<, this->m_cBackBuffers)*/ UINT iBackBuffer,
    __in_ecount(1) const CMilRectU& rcDirty,
    __deref_out HDC *phdcBackBuffer
    ) const
{
    HRESULT hr = S_OK;

    ENTER_USE_CONTEXT_FOR_SCOPE(Device());

    Assert(iBackBuffer < m_cBackBuffers);

    D3DSURFACE_DESC const &surfDesc = m_rgBackBuffers[iBackBuffer]->Desc();

    UINT cbBufferInset =
          m_stride * rcDirty.top
        + D3DFormatSize(surfDesc.Format) * rcDirty.left;

    BYTE *pbBuffer = reinterpret_cast<BYTE*>(m_pBuffer) + cbBufferInset;

    IFC(m_rgBackBuffers[iBackBuffer]->ReadIntoSysMemBuffer(
        rcDirty,
        0,
        NULL,
        D3DFormatToPixelFormat(surfDesc.Format, TRUE),
        m_stride,
        DBG_ANALYSIS_PARAM_COMMA(m_cbBuffer - cbBufferInset)
        pbBuffer
        ));

    *phdcBackBuffer = m_hdcCopiedBackBuffer;

Cleanup:
    RRETURN(hr);
}

上面代码的核心逻辑是 ReadIntoSysMemBuffer 方法,从这里进行了内存的拷贝。这里也就能回答大家为什么会使用更多的 CPU 的原因了,此时存在了显存(这个说法不一定对)到内存的拷贝,进行一次 4k 的大图拷贝的效率还是很低的。当然了,对于没有显存的设备来说,依然也是需要 CPU 到 CPU 的拷贝

好在 WPF 还是加了一点优化的,只是拷贝 rcDirty 范围而已,这个变量的命名意思是 rect (rc) 矩形的 Dirty 需要重绘的范围

回到 CD3DDeviceLevel1::PresentWithGDI 方法,在拿到 hdcBackBuffer 之后,此时就可以使用 hdcBackBuffer 进行 GDI 渲染了。调用的核心方法是 UpdateLayeredWindowEx 方法。这里的 UpdateLayeredWindowEx 是放在 oscompat.cpp 文件里,这个代码是为了做系统兼容使用的,本质就是将会通过系统判断,调用 UpdateLayeredWindowUpdateLayeredWindowIndirect 方法,如下面代码

//+----------------------------------------------------------------------------
//
//  Function:  UpdateLayeredWindowEx
//
//  Synopsis:  Call UpdateLayeredWindow or UpdateLayeredWindowIndirect as
//             required by parameters.  If UpdateLayeredWindowIndirect is
//             needed (ULW_EX_NORESIZE requested), but not available return
//             HRESULT_FROM_WIN32(ERROR_PROC_NOT_FOUND).  prcDirty is ignored
//             when UpdateLayeredWindowIndirect is not available.
//
//-----------------------------------------------------------------------------
HRESULT
UpdateLayeredWindowEx(
    __in HWND hWnd,
    __in_opt HDC hdcDst,
    __in_ecount_opt(1) CONST POINT *pptDst,
    __in_ecount_opt(1) CONST SIZE *psize,
    __in_opt HDC hdcSrc,
    __in_ecount_opt(1) CONST POINT *pptSrc,
    COLORREF crKey,
    __in_ecount_opt(1)CONST BLENDFUNCTION *pblend,
    DWORD dwFlags,
    __in_ecount_opt(1) CONST RECT *prcDirty
    )

而在 Windows 提供的 UpdateLayeredWindowUpdateLayeredWindowIndirect 方法将会支持传入 GDI 的绘图空间,根据给定的颜色设置透明。详细使用方法请看 分层窗口UpdateLayeredWindowIndirect局部更新

也就是说整个 WPF 的 AllowsTransparency 设置透明的一个最底层核心逻辑就是调用 UpdateLayeredWindowUpdateLayeredWindowIndirect 方法实现

在调用过程中需要从 DX 将窗口渲染内容拷贝出来放在内存,然后使用 GDI 进行渲染。在拷贝内存过程中需要重新申请一段内存空间,将会在窗口比较大的时候占用更多的内存,同时拷贝需要使用更多的 CPU 计算。而通过 GDI 的再次渲染将会降低整个应用的渲染性能

说到这里,是否有方法可以提升性能?其实有的,详细请看 WPF 制作高性能的透明背景异形窗口

当前的 WPF 在 https://github.com/dotnet/wpf 完全开源,使用友好的 MIT 协议,意味着允许任何人任何组织和企业任意处置,包括使用,复制,修改,合并,发表,分发,再授权,或者销售。在仓库里面包含了完全的构建逻辑,只需要本地的网络足够好(因为需要下载一堆构建工具),即可进行本地构建

我搭建了自己的博客 https://blog.lindexi.com/ 欢迎大家访问,里面有很多新的博客。只有在我看到博客写成熟之后才会放在csdn或博客园,但是一旦发布了就不再更新

如果在博客看到有任何不懂的,欢迎交流,我搭建了 dotnet 职业技术学院 欢迎大家加入

如有不方便在博客评论的问题,可以加我 QQ 2844808902 交流

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接:http://blog.csdn.net/lindexi_gd ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系。

lindexi_gd CSDN认证博客专家 C# WPF UWP 微软最具价值专家
我是微软Windows应用开发方向的最具价值专家,欢迎访问我博客blog.lindexi.com里面有大量WPF和UWP博客
已标记关键词 清除标记
相关推荐
<p> <strong><span style="font-size:16px;color:#003399;">会用Python分析金融数据 or 金融行业会用Python</span></strong> </p> <p> <strong><span style="font-size:16px;color:#003399;">职场竞争力更高</span></strong> </p> <p> <br /> </p> <p> <img src="https://img-bss.csdnimg.cn/202012231042221925.png" alt="" /> </p> <p> <br /> </p> <p> <br /> </p> <p> <strong><span style="font-size:16px;color:#003399;">Python金融数据分析入门到实战</span></strong> </p> <p> <strong><span style="font-size:16px;color:#003399;">Get√金融行业数据分析必备技能</span></strong> </p> <p> <img src="https://img-bss.csdnimg.cn/202012231042438069.png" alt="" /> </p> <p> <br /> </p> <p> <br /> </p> <p> <strong><span style="font-size:16px;color:#003399;">以股票量化交易为应用场景</span></strong> </p> <p> <strong><span style="font-size:16px;color:#003399;">完成技术指标实现的全过程</span></strong> </p> <p> <br /> </p> <p> <span style="font-size:14px;">课程选取股票量化交易为应用场景,由股票数据的获取、技术指标的实现,逐步进阶到策略的设计</span><span style="font-size:14px;">和回测,由浅入深、由技术到思维地为同学们讲解Python金融数据分析在股票量化交易中的应用</span><span style="font-size:14px;">。</span> </p> <p> <br /> </p> <p> <span style="font-size:14px;"><br /> </span> </p> <p> <img src="https://img-bss.csdnimg.cn/202012231043183686.png" alt="" /> </p> <p> <br /> </p> <p> <br /> </p> <p> <strong><span style="font-size:16px;color:#003399;">以Python为编程语言</span></strong> </p> <p> <strong><span style="font-size:16px;color:#003399;">解锁3大主流数据分析工具</span></strong> </p> <p> <br /> </p> <p> <span style="font-size:14px;">Python做金融具有先天优势,课程提取了Python数据分析工具NumPy、Pandas及可视化工具</span><span style="font-size:14px;">Matplotlib的关键点详细讲解,帮助同学掌握数据分析的关键技能。</span> </p> <p> <img src="https://img-bss.csdnimg.cn/202012231043472858.png" alt="" /> </p> <p> <strong><span style="font-size:16px;color:#003399;"><br /> </span></strong> </p> <p> <strong><span style="font-size:16px;color:#003399;">2大购课福利</span></strong> </p> <p> <strong><span style="font-size:16px;color:#003399;"><br /> </span></strong> </p> <p> <img src="https://img-bss.csdnimg.cn/202012300628195864.png" alt="" /> </p>
©️2020 CSDN 皮肤主题: 终极编程指南 设计师:CSDN官方博客 返回首页