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

On Windows Vista or later operating system, use the Direct3DCreate9Ex method 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

The d3d9.dll has the Direct3DCreate9Ex method only on Windows Vista or later operating system. 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 and later operating systems, which are 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

The D3DImage class manages two display buffers, which are called the back buffer and the front buffer. The back buffer is your Direct3D surface. Changes to the back buffer are copied forward to the front buffer when you call the Unlock method, where it is displayed on the hardware. Occasionally, the front buffer becomes unavailable. This lack of availability can be caused by screen locking, full-screen exclusive Direct3D applications, user-switching, or other system activities. When this occurs, your WPF application is notified by handling the IsFrontBufferAvailableChanged event. How your application responds to the front buffer becoming unavailable depends on whether WPF is enabled to fall back to software rendering. The SetBackBuffer method has an overload that takes a parameter that specifies whether WPF falls back to software rendering.

When you call the SetBackBuffer(D3DResourceType, IntPtr) overload or call the SetBackBuffer(D3DResourceType, IntPtr, Boolean) overload with the enableSoftwareFallback parameter set to false, the rendering system releases its reference to the back buffer when the front buffer becomes unavailable and nothing is displayed. When the front buffer is available again, the rendering system raises the IsFrontBufferAvailableChanged event to notify your WPF application. You can create an event handler for the IsFrontBufferAvailableChanged event to restart rendering again with a valid Direct3D surface. To restart rendering, you must call SetBackBuffer.

When you call the SetBackBuffer(D3DResourceType, IntPtr, Boolean) overload with the enableSoftwareFallback parameter set to true, the rendering system retains its reference to the back buffer when the front buffer becomes unavailable, so there is no need to call SetBackBuffer when the front buffer is available again.

When software rendering is enabled, there may be situations where the user’s device becomes unavailable, but the rendering system retains a reference to the Direct3D surface. To check whether a Direct3D9 device is unavailable, call the TestCooperativeLevel method. To check a Direct3D9Ex devices call the CheckDeviceState method, because the TestCooperativeLevel method is deprecated and always returns success. If the user device has become unavailable, call SetBackBuffer to release WPF’s reference to the back buffer. If you need to reset your device, call SetBackBuffer with the backBuffer parameter set to null, and then call SetBackBuffer again with backBuffer set to a valid Direct3D surface.

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