你可以在 Windows Presentation Foundation (WPF) 应用程序中包括 Direct3D9 内容。 本主题介绍如何创建 Direct3D9 内容,以便它与 WPF 高效互操作。
显示缓冲区
D3DImage 类管理两个显示缓冲区,这些缓冲区称为 后缓冲区 和 前缓冲区。 后台缓冲区是 Direct3D9 图面。 调用 Unlock 方法时,对后台缓冲区所做的更改将转发到前缓冲区。
下图显示了后缓冲区与前缓冲区之间的关系。
Direct3D9 设备创建
若要呈现 Direct3D9 内容,必须创建 Direct3D9 设备。 有两个 Direct3D9 对象可用于创建设备、IDirect3D9
和 IDirect3D9Ex
。 使用这些对象分别创建 IDirect3DDevice9
和 IDirect3DDevice9Ex
设备。
通过调用以下方法之一创建设备。
IDirect3D9 * Direct3DCreate9(UINT SDKVersion);
HRESULT Direct3DCreate9Ex(UINT SDKVersion, IDirect3D9Ex **ppD3D);
在 Windows Vista 或更高版本的操作系统上,将 Direct3DCreate9Ex
方法与配置为使用 Windows 显示驱动程序模型(WDDM)的显示器配合使用。 对任何其他平台使用 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
结构,但只有几个参数很重要。 选择这些参数可以最大程度地减少内存占用。
将 BackBufferHeight
和 BackBufferWidth
字段设置为 1。 将其设置为 0 会导致其设置为 HWND 的维度。
始终设置 D3DCREATE_MULTITHREADED
和 D3DCREATE_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
方法已弃用并且总是返回成功结果。 如果用户设备不可用,请调用 SetBackBuffer 释放 WPF 对后缓冲区的引用。 如果需要重置设备,请调用 SetBackBuffer 并将 backBuffer
参数设置为 null
,然后再次调用 SetBackBuffer 并将 backBuffer
设置为有效的 Direct3D 图面。
仅当实现多适配器支持时,才调用 Reset
方法从无效设备中恢复。 否则,请释放所有 Direct3D9 接口并完全重新创建它们。 如果适配器布局已更改,则不会更新更改之前创建的 Direct3D9 对象。
处理调整大小
如果 D3DImage 以其本机大小以外的分辨率显示,则会根据当前 BitmapScalingMode 对其进行缩放,但 Bilinear 会被替换为 Fant。
如果需要更高的保真度,则必须在 D3DImage 的容器更改大小时创建一个新图面。
有三种可能的方法来处理调整大小。
参与布局系统,并在大小发生变化时创建新的图面。 不要创建过多的图面,因为可能会耗尽或碎片视频内存。
等到固定时间段内未发生调整大小事件时,再创建新的图面。
创建一个 DispatcherTimer,用于每秒检查几次容器维度。
多显示器优化
当渲染系统将 D3DImage 移动到另一台监视器时,性能可能会显著下降。
在 WDDM 上,只要监视器位于同一个视频卡上,并且使用 Direct3DCreate9Ex
,性能不会降低。 如果监视器位于单独的视频卡上,则性能会降低。 在 Windows XP 上,性能始终会降低。
当 D3DImage 移动到另一个显示器时,可以在相应的适配器上创建新图面,以恢复良好的性能。
为了避免性能损失,请专门为多监视器情况编写代码。 以下列表显示了编写多监视器代码的一种方法。
使用 D3DImage 方法在屏幕空间中查找
Visual.ProjectToScreen
点。使用
MonitorFromPoint
GDI 方法查找显示该点的监视器。使用
IDirect3D9::GetAdapterMonitor
方法查找显示器所在的 Direct3D9 适配器。如果适配器与使用后缓冲区的适配器不同,请在新监视器上创建新的后退缓冲区,并将其分配给 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 没有前缓冲区。