WPF 和 Direct3D9 互操作

你可以在 Windows Presentation Foundation (WPF) 应用程序中包含 Direct3D9 内容。 本主题介绍如何创建 Direct3D9 内容,以便它有效地与 WPF 互操作。

注意

在 WPF 中使用 Direct3D9 内容时,还需要考虑性能问题。 有关如何优化性能的详细信息,请参阅 Direct3D9 和 WPF 互操作性的性能注意事项

显示缓冲区

D3DImage 类管理两个显示缓冲区,称为后台缓冲区和前台缓冲区。 后台缓冲区是 Direct3D9 图面。 调用 Unlock 方法时,对后台缓冲区的更改会向前复制到前台缓冲区。

下图显示了后台缓冲区和前台缓冲区之间的关系。

D3DImage display buffers

Direct3D9 设备创建

若要呈现 Direct3D9 内容,必须创建 Direct3D9 设备。 有两个 Direct3D9 对象可用于创建设备,即 IDirect3D9IDirect3D9Ex。 可使用这些对象分别创建 IDirect3DDevice9IDirect3DDevice9Ex 设备。

通过调用以下方法之一创建设备。

  • IDirect3D9 * Direct3DCreate9(UINT SDKVersion);

  • HRESULT Direct3DCreate9Ex(UINT SDKVersion, IDirect3D9Ex **ppD3D);

在 Windows Vista 或更高版本的操作系统上,对配置为使用 Windows 显示驱动程序模型 (WDDM) 的显示器使用 Direct3DCreate9Ex 方法。 对任何其他平台使用 Direct3DCreate9 方法。

Direct3DCreate9Ex 方法的可用性

d3d9.dll 仅在 Windows Vista 或更高版本的操作系统上提供 Direct3DCreate9Ex 方法。 如果在 Windows XP 上直接链接该函数,应用程序将无法加载。 若要确定是否支持 Direct3DCreate9Ex 方法,请加载 DLL 并查找 proc 地址。 以下代码演示如何测试 Direct3DCreate9Ex 方法。 有关完整代码示例,请参阅演练:创建在 WPF 中承载的 Direct3D9 内容

HRESULT
CRendererManager::EnsureD3DObjects()
{
    HRESULT hr = S_OK;

    HMODULE hD3D = NULL;
    if (!m_pD3D)
    {
        hD3D = LoadLibrary(TEXT("d3d9.dll"));
        DIRECT3DCREATE9EXFUNCTION pfnCreate9Ex = (DIRECT3DCREATE9EXFUNCTION)GetProcAddress(hD3D, "Direct3DCreate9Ex");
        if (pfnCreate9Ex)
        {
            IFC((*pfnCreate9Ex)(D3D_SDK_VERSION, &m_pD3DEx));
            IFC(m_pD3DEx->QueryInterface(__uuidof(IDirect3D9), reinterpret_cast<void **>(&m_pD3D)));
        }
        else
        {
            m_pD3D = Direct3DCreate9(D3D_SDK_VERSION);
            if (!m_pD3D) 
            {
                IFC(E_FAIL);
            }
        }

        m_cAdapters = m_pD3D->GetAdapterCount();
    }

Cleanup:
    if (hD3D)
    {
        FreeLibrary(hD3D);
    }

    return hr;
}

HWND 创建

创建设备需要 HWND。 通常,需要创建一个虚拟 HWND 供 Direct3D9 使用。 以下代码示例演示如何创建虚拟 HWND。

HRESULT
CRendererManager::EnsureHWND()
{
    HRESULT hr = S_OK;

    if (!m_hwnd)
    {
        WNDCLASS wndclass;

        wndclass.style = CS_HREDRAW | CS_VREDRAW;
        wndclass.lpfnWndProc = DefWindowProc;
        wndclass.cbClsExtra = 0;
        wndclass.cbWndExtra = 0;
        wndclass.hInstance = NULL;
        wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
        wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
        wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH);
        wndclass.lpszMenuName = NULL;
        wndclass.lpszClassName = szAppName;

        if (!RegisterClass(&wndclass))
        {
            IFC(E_FAIL);
        }

        m_hwnd = CreateWindow(szAppName,
                            TEXT("D3DImageSample"),
                            WS_OVERLAPPEDWINDOW,
                            0,                   // Initial X
                            0,                   // Initial Y
                            0,                   // Width
                            0,                   // Height
                            NULL,
                            NULL,
                            NULL,
                            NULL);
    }

Cleanup:
    return hr;
}

呈现参数

创建设备还需要有 D3DPRESENT_PARAMETERS 结构,但只有几个参数很重要。 选择这些参数是为了最小化内存占用。

BackBufferHeightBackBufferWidth 字段设置为 1。 将其设置为 0 会导致其设置为 HWND 的维度。

始终设置 D3DCREATE_MULTITHREADEDD3DCREATE_FPU_PRESERVE 标记,以防止损坏 Direct3D9 所用的内存,并防止 Direct3D9 更改 FPU 设置。

以下代码演示如何初始化 D3DPRESENT_PARAMETERS 结构。

HRESULT 
CRenderer::Init(IDirect3D9 *pD3D, IDirect3D9Ex *pD3DEx, HWND hwnd, UINT uAdapter)
{
    HRESULT hr = S_OK;

    D3DPRESENT_PARAMETERS d3dpp;
    ZeroMemory(&d3dpp, sizeof(d3dpp));
    d3dpp.Windowed = TRUE;
    d3dpp.BackBufferFormat = D3DFMT_UNKNOWN;
    d3dpp.BackBufferHeight = 1;
    d3dpp.BackBufferWidth = 1;
    d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;

    D3DCAPS9 caps;
    DWORD dwVertexProcessing;
    IFC(pD3D->GetDeviceCaps(uAdapter, D3DDEVTYPE_HAL, &caps));
    if ((caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT) == D3DDEVCAPS_HWTRANSFORMANDLIGHT)
    {
        dwVertexProcessing = D3DCREATE_HARDWARE_VERTEXPROCESSING;
    }
    else
    {
        dwVertexProcessing = D3DCREATE_SOFTWARE_VERTEXPROCESSING;
    }

    if (pD3DEx)
    {
        IDirect3DDevice9Ex *pd3dDevice = NULL;
        IFC(pD3DEx->CreateDeviceEx(
            uAdapter,
            D3DDEVTYPE_HAL,
            hwnd,
            dwVertexProcessing | D3DCREATE_MULTITHREADED | D3DCREATE_FPU_PRESERVE,
            &d3dpp,
            NULL,
            &m_pd3dDeviceEx
            ));

        IFC(m_pd3dDeviceEx->QueryInterface(__uuidof(IDirect3DDevice9), reinterpret_cast<void**>(&m_pd3dDevice)));  
    }
    else 
    {
        assert(pD3D);

        IFC(pD3D->CreateDevice(
            uAdapter,
            D3DDEVTYPE_HAL,
            hwnd,
            dwVertexProcessing | D3DCREATE_MULTITHREADED | D3DCREATE_FPU_PRESERVE,
            &d3dpp,
            &m_pd3dDevice
            ));
    }

Cleanup:
    return hr;
}

创建后台缓冲区呈现目标

若要在 D3DImage 中显示 Direct3D9 内容,请创建一个 Direct3D9 图面并通过调用 SetBackBuffer 方法来分配它。

验证适配器支持

在创建图面之前,请验证所有适配器是否都支持所需的图面属性。 即使只呈现到一个适配器,WPF 窗口也可能显示在系统中的任何适配器上。 应始终编写处理多适配器配置的 Direct3D9 代码,并且应检查所有适配器是否提供支持,因为 WPF 可能会在可用适配器之间移动图面。

以下代码示例演示如何检查系统上的所有适配器是否支持 Direct3D9。

HRESULT
CRendererManager::TestSurfaceSettings()
{
    HRESULT hr = S_OK;

    D3DFORMAT fmt = m_fUseAlpha ? D3DFMT_A8R8G8B8 : D3DFMT_X8R8G8B8;

    // 
    // We test all adapters because because we potentially use all adapters.
    // But even if this sample only rendered to the default adapter, you
    // should check all adapters because WPF may move your surface to
    // another adapter for you!
    //

    for (UINT i = 0; i < m_cAdapters; ++i)
    {
        // Can we get HW rendering?
        IFC(m_pD3D->CheckDeviceType(
            i,
            D3DDEVTYPE_HAL,
            D3DFMT_X8R8G8B8,
            fmt,
            TRUE
            )); 

        // Is the format okay?
        IFC(m_pD3D->CheckDeviceFormat(
            i,
            D3DDEVTYPE_HAL,
            D3DFMT_X8R8G8B8,
            D3DUSAGE_RENDERTARGET | D3DUSAGE_DYNAMIC, // We'll use dynamic when on XP
            D3DRTYPE_SURFACE,
            fmt
            ));

        // D3DImage only allows multisampling on 9Ex devices. If we can't 
        // multisample, overwrite the desired number of samples with 0.
        if (m_pD3DEx && m_uNumSamples > 1)
        {   
            assert(m_uNumSamples <= 16);

            if (FAILED(m_pD3D->CheckDeviceMultiSampleType(
                i,
                D3DDEVTYPE_HAL,
                fmt,
                TRUE,
                static_cast<D3DMULTISAMPLE_TYPE>(m_uNumSamples),
                NULL
                )))
            {
                m_uNumSamples = 0;
            }
        }
        else
        {
            m_uNumSamples = 0;
        }
    }

Cleanup:
    return hr;
}

创建图面

在创建图面之前,请验证设备功能是否支持目标操作系统上的良好性能。 有关详细信息,请参阅 Direct3D9 和 WPF 互操作性的性能注意事项

验证设备功能后,即可创建图面。 以下代码示例演示如何创建呈现目标。

HRESULT
CRenderer::CreateSurface(UINT uWidth, UINT uHeight, bool fUseAlpha, UINT m_uNumSamples)
{
    HRESULT hr = S_OK;

    SAFE_RELEASE(m_pd3dRTS);

    IFC(m_pd3dDevice->CreateRenderTarget(
        uWidth,
        uHeight,
        fUseAlpha ? D3DFMT_A8R8G8B8 : D3DFMT_X8R8G8B8,
        static_cast<D3DMULTISAMPLE_TYPE>(m_uNumSamples),
        0,
        m_pd3dDeviceEx ? FALSE : TRUE,  // Lockable RT required for good XP perf
        &m_pd3dRTS,
        NULL
        ));

    IFC(m_pd3dDevice->SetRenderTarget(0, m_pd3dRTS));

Cleanup:
    return hr;
}

WDDM

在配置为使用 WDDM 的 Windows Vista 及更高版本的操作系统上,可以创建呈现目标纹理并将 0 级图面传递给 SetBackBuffer 方法。 不建议在 Windows XP 上使用此方法,因为你无法创建可锁定的呈现目标纹理,并且性能会降低。

处理设备状态

D3DImage 类管理两个显示缓冲区,称为后台缓冲区和前台缓冲区。 后台缓冲区是 Direct3D 图面。 调用 Unlock 方法时,对后台缓冲区的更改会向前复制到前台缓冲区,并在硬件上显示。 有时,前台缓冲区会变得不可用。 导致其不可用的原因可能包括屏幕锁定、全屏独占 Direct3D 应用程序、用户切换或其他系统活动。 发生这种情况时,WPF 应用程序会通过处理 IsFrontBufferAvailableChanged 事件收到通知。 应用程序如何响应变得不可用的前台缓冲区取决于 WPF 是否能够回退到软件呈现。 SetBackBuffer 方法有一个重载,该重载使用的参数指定了 WPF 是否回退到软件呈现。

调用 SetBackBuffer(D3DResourceType, IntPtr) 重载或调用 SetBackBuffer(D3DResourceType, IntPtr, Boolean) 重载并将 enableSoftwareFallback 参数设置为 false 时,呈现系统会在前台缓冲区不可用且不显示任何内容时释放其对后台缓冲区的引用。 当前台缓冲区再次可用时,呈现系统会引发 IsFrontBufferAvailableChanged 事件以通知 WPF 应用程序。 你可以为 IsFrontBufferAvailableChanged 事件创建事件处理程序,以使用有效的 Direct3D 图面重新开始呈现。 若要重新开始呈现,必须调用 SetBackBuffer

调用 SetBackBuffer(D3DResourceType, IntPtr, Boolean) 重载并将 enableSoftwareFallback 参数设置为 true 时,呈现系统会在前台缓冲区不可用时保留其对后台缓冲区的引用,因此当前台缓冲区再次可用时无需调用 SetBackBuffer

启用软件呈现后,可能会出现用户设备不可用的情况,但呈现系统会保留对 Direct3D 图面的引用。 若要检查 Direct3D9 设备是否不可用,请调用 TestCooperativeLevel 方法。 若要检查 Direct3D9Ex 设备,请调用 CheckDeviceState 方法,因为 TestCooperativeLevel 方法已被弃用,并且始终返回 success。 如果用户设备不可用,请调用 SetBackBuffer 以释放 WPF 对后台缓冲区的引用。 如果需要重置设备,请调用 SetBackBuffer 并将 backBuffer 参数设置为 null,然后再次调用 SetBackBuffer 并将 backBuffer 设置为有效的 Direct3D 图面。

仅当实现多适配器支持时,才调用 Reset 方法从无效设备中恢复。 否则,释放所有 Direct3D9 接口并完全重新创建。 如果适配器布局已更改,则在更改之前创建的 Direct3D9 对象不会更新。

处理调整大小

如果 D3DImage 以其本机大小以外的分辨率显示,则会根据当前 BitmapScalingMode 对其进行缩放,但 Fant 会被替换为 Bilinear

如果需要更高的保真度,则必须在 D3DImage 的容器更改大小时创建一个新图面。

有三种可能的方法来处理调整大小。

  • 参与布局系统,并在大小发生变化时创建新的图面。 不要创建太多图面,因为这样可能会耗尽视频内存或使内存碎片化。

  • 等到固定时间段内未发生调整大小事件时,再创建新的图面。

  • 创建一个 DispatcherTimer,用于每秒检查几次容器维度。

多显示器优化

当呈现系统将 D3DImage 移动到另一个显示器时,性能会显著降低。

在 WDDM 上,只要显示器在同一个视频卡上并且你使用 Direct3DCreate9Ex,性能就不会降低。 如果显示器位于不同的视频卡上,性能会降低。 在 Windows XP 上,性能始终会降低。

D3DImage 移动到另一个显示器时,可以在相应的适配器上创建新图面,以恢复良好的性能。

为避免性能损失,可专门为多显示器编写代码。 以下列表介绍了一种编写多显示器代码的方法。

  1. 使用 Visual.ProjectToScreen 方法在屏幕空间中查找 D3DImage 的一个点。

  2. 使用 MonitorFromPoint GDI 方法查找正在显示该点的显示器。

  3. 使用 IDirect3D9::GetAdapterMonitor 方法查找显示器所在的 Direct3D9 适配器。

  4. 如果适配器与具有后台缓冲区的适配器不同,则在新显示器上创建一个新的后台缓冲区,并将其分配给 D3DImage 后台缓冲区。

注意

如果 D3DImage 跨显示器,性能会很慢,除非 WDDM 和 IDirect3D9Ex 在同一个适配器上。 在这种情况下,无法提高性能。

以下代码示例演示如何查找当前显示器。

void 
CRendererManager::SetAdapter(POINT screenSpacePoint)
{
    CleanupInvalidDevices();

    //
    // After CleanupInvalidDevices, we may not have any D3D objects. Rather than
    // recreate them here, ignore the adapter update and wait for render to recreate.
    //

    if (m_pD3D && m_rgRenderers)
    {
        HMONITOR hMon = MonitorFromPoint(screenSpacePoint, MONITOR_DEFAULTTONULL);

        for (UINT i = 0; i < m_cAdapters; ++i)
        {
            if (hMon == m_pD3D->GetAdapterMonitor(i))
            {
                m_pCurrentRenderer = m_rgRenderers[i];
                break;
            }
        }
    }
}

D3DImage 容器的大小或位置发生变化时更新显示器,或者使用每秒更新几次的 DispatcherTimer 更新显示器。

WPF 软件呈现

在以下情况下,WPF 会在软件中的 UI 线程上同步呈现。

出现其中一种情况时,呈现系统会调用 CopyBackBuffer 方法将硬件缓冲区复制到软件。 默认实现使用图面调用 GetRenderTargetData 方法。 此调用发生在锁定/解锁模式之外,因此它可能会失败。 在这种情况下,CopyBackBuffer 方法会返回 null 并且不显示任何图像。

你可以替代 CopyBackBuffer 方法,调用基本实现,如果它返回 null,你可以返回占位符 BitmapSource

你还可以实现自己的软件呈现,而不是调用基本实现。

注意

如果 WPF 在软件中完全呈现,则不会显示 D3DImage,因为 WPF 没有前台缓冲区。

另请参阅