WPF and Direct3D9 Interoperation

You can include Direct3D9 content in a Windows Presentation Foundation (WPF) application. This topic describes how to create Direct3D9 content so that it efficiently interoperates with WPF.

Note

When using Direct3D9 content in WPF, you also need to think about performance. For more information about how to optimize for performance, see Performance Considerations for Direct3D9 and WPF Interoperability.

Display Buffers

The D3DImage class manages two display buffers, which are called the back buffer and the front buffer. The back buffer is your Direct3D9 surface. Changes to the back buffer are copied forward to the front buffer when you call the Unlock method.

The following illustration shows the relationship between the back buffer and the front buffer.

D3DImage display buffers

Direct3D9 Device Creation

To render Direct3D9 content, you must create a Direct3D9 device. There are two Direct3D9 objects that you can use to create a device, IDirect3D9 and IDirect3D9Ex. Use these objects to create IDirect3DDevice9 and IDirect3DDevice9Ex devices, respectively.

Create a device by calling one of the following methods.

  • IDirect3D9 * Direct3DCreate9(UINT SDKVersion);

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

Use the Direct3DCreate9Ex method on Windows Vista with a display that is configured to use the Windows Display Driver Model (WDDM). Use the Direct3DCreate9 method on any other platform.

Availability of the Direct3DCreate9Ex method

Only the d3d9.dll on Windows Vista has the Direct3DCreate9Ex method. If you directly link the function on Windows XP, your application fails to load. To determine whether the Direct3DCreate9Ex method is supported, load the DLL and look for the proc address. The following code shows how to test for the Direct3DCreate9Ex method. For a full code example, see Walkthrough: Creating Direct3D9 Content for Hosting in WPF.

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 Creation

Creating a device requires an HWND. In general, you create a dummy HWND for Direct3D9 to use. The following code example shows how to create a dummy 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;
}

Present Parameters

Creating a device also requires a D3DPRESENT_PARAMETERS struct, but only a few parameters are important. These parameters are chosen to minimize the memory footprint.

Set the BackBufferHeight and BackBufferWidth fields to 1. Setting them to 0 causes them to be set to the dimensions of the HWND.

Always set the D3DCREATE_MULTITHREADED and D3DCREATE_FPU_PRESERVE flags to prevent corrupting memory used by Direct3D9 and to prevent Direct3D9 from changing FPU settings.

The following code shows how to initialize the D3DPRESENT_PARAMETERS struct.

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;
}

Creating the Back Buffer Render Target

To display Direct3D9 content in a D3DImage, you create a Direct3D9 surface and assign it by calling the SetBackBuffer method.

Verifying Adapter Support

Before creating a surface, verify that all adapters support the surface properties you require. Even if you render to only one adapter, the WPF window may be displayed on any adapter in the system. You should always write Direct3D9 code that handles multi-adapter configurations, and you should check all adapters for support, because WPF might move the surface among the available adapters.

The following code example shows how to check all adapters on the system for Direct3D9 support.

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;
}

Creating the Surface

Before creating a surface, verify that the device capabilities support good performance on the target operating system. For more information, see Performance Considerations for Direct3D9 and WPF Interoperability.

When you have verified device capabilities, you can create the surface. The following code example shows how to create the render target.

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

On Windows Vista, which is configured to use the WDDM, you can create a render target texture and pass the level 0 surface to the SetBackBuffer method. This approach is not recommended on Windows XP, because you cannot create a lockable render target texture and performance will be reduced.

Handling Device State

When the IsFrontBufferAvailable property transitions from true to false, WPF does not display the D3DImage and it does not copy your back buffer to the front buffer. This usually means that the device has been lost.

When the device is lost, your code should stop rendering and wait for the IsFrontBufferAvailable property to transition to true. Handle the IsFrontBufferAvailableChanged event to be notified of this transition.

When the IsFrontBufferAvailable property transitions from false to true, you should check your device to determine whether it is valid.

For Direct3D9 devices, call the TestCooperativeLevel method. For Direct3D9Ex devices call the CheckDeviceState method, because the TestCooperativeLevel method is deprecated and always returns success.

If the device is valid, call the SetBackBuffer method again with the original surface.

If the device is not valid, you must reset your device and re-create your resources. Calling the SetBackBuffer method with a surface from an invalid device raises an exception.

Call the Reset method to recover from an invalid device only if you implement multi-adapter support. Otherwise, release all Direct3D9 interfaces and re-create them completely. If the adapter layout has changed, Direct3D9 objects created before the change are not updated.

Handling Resizing

If a D3DImage is displayed at a resolution other than its native size, it is scaled according to the current BitmapScalingMode, except that Bilinear is substituted for Fant.

If you require higher fidelity, you must create a new surface when the container of the D3DImage changes size.

There are three possible approaches to handle resizing.

  • Participate in the layout system and create a new surface when the size changes. Do not create too many surfaces, because you may exhaust or fragment video memory.

  • Wait until a resize event has not occurred for a fixed period of time to create the new surface.

  • Create a DispatcherTimer that checks the container dimensions several times per second.

Multi-monitor Optimization

Significantly reduced performance can result when the rendering system moves a D3DImage to another monitor.

On WDDM, as long as the monitors are on the same video card and you use Direct3DCreate9Ex, there is no reduction in performance. If the monitors are on separate video cards, performance is reduced. On Windows XP, performance is always reduced.

When the D3DImage moves to another monitor, you can create a new surface on the corresponding adapter to restore good performance.

To avoid the performance penalty, write code specifically for the multi-monitor case. The following list shows one way to write multi-monitor code.

  1. Find a point of the D3DImage in screen space with the Visual.ProjectToScreen method.

  2. Use the MonitorFromPoint GDI method to find the monitor that is displaying the point.

  3. Use the IDirect3D9::GetAdapterMonitor method to find which Direct3D9 adapter the monitor is on.

  4. If the adapter is not the same as the adapter with the back buffer, create a new back buffer on the new monitor and assign it to the D3DImage back buffer.

Note

If the D3DImage straddles monitors, performance will be slow, except in the case of WDDM and IDirect3D9Ex on the same adapter. There is no way to improve performance in this situation.

The following code example shows how to find the current monitor.

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;
            }
        }
    }
}

Update the monitor when the D3DImage container's size or position changes, or update the monitor by using a DispatcherTimer that updates a few times per second.

WPF Software Rendering

WPF renders synchronously on the UI thread in software in the following situations.

When one of these situations occurs, the rendering system calls the CopyBackBuffer method to copy the hardware buffer to software. The default implementation calls the GetRenderTargetData method with your surface. Because this call occurs outside of the Lock/Unlock pattern, it may fail. In this case, the CopyBackBuffer method returns null and no image is displayed.

You can override the CopyBackBuffer method, call the base implementation, and if it returns null, you can return a placeholder BitmapSource.

You can also implement your own software rendering instead of calling the base implementation.

Note

If WPF is rendering completely in software, D3DImage is not shown because WPF does not have a front buffer.

See Also

Tasks

Walkthrough: Creating Direct3D9 Content for Hosting in WPF

Walkthrough: Hosting Direct3D9 Content in WPF

Reference

D3DImage

Concepts

Performance Considerations for Direct3D9 and WPF Interoperability