在 DirectShow 中支持 DXVA 2.0
本主题介绍如何在 DirectShow 解码器筛选器中支持 DirectX 视频加速 (DXVA) 2.0。 具体而言,它描述了解码器和视频呈现器之间的通信。 本主题不介绍如何实现 DXVA 解码。
先决条件
本主题假定你熟悉编写 DirectShow 筛选器。 有关详细信息,请参阅 DirectShow SDK 文档中的主题编写 DirectShow 筛选器。 本主题中的代码示例假定解码器筛选器派生自 CTransformFilter 类,并具有以下类定义:
class CDecoder : public CTransformFilter
{
public:
static CUnknown* WINAPI CreateInstance(IUnknown *pUnk, HRESULT *pHr);
HRESULT CompleteConnect(PIN_DIRECTION direction, IPin *pPin);
HRESULT InitAllocator(IMemAllocator **ppAlloc);
HRESULT DecideBufferSize(IMemAllocator *pAlloc, ALLOCATOR_PROPERTIES *pProp);
// TODO: The implementations of these methods depend on the specific decoder.
HRESULT CheckInputType(const CMediaType *mtIn);
HRESULT CheckTransform(const CMediaType *mtIn, const CMediaType *mtOut);
HRESULT CTransformFilter::GetMediaType(int,CMediaType *);
private:
CDecoder(HRESULT *pHr);
~CDecoder();
CBasePin * GetPin(int n);
HRESULT ConfigureDXVA2(IPin *pPin);
HRESULT SetEVRForDXVA2(IPin *pPin);
HRESULT FindDecoderConfiguration(
/* [in] */ IDirectXVideoDecoderService *pDecoderService,
/* [in] */ const GUID& guidDecoder,
/* [out] */ DXVA2_ConfigPictureDecode *pSelectedConfig,
/* [out] */ BOOL *pbFoundDXVA2Configuration
);
private:
IDirectXVideoDecoderService *m_pDecoderService;
DXVA2_ConfigPictureDecode m_DecoderConfig;
GUID m_DecoderGuid;
HANDLE m_hDevice;
FOURCC m_fccOutputFormat;
};
在本主题的其余部分, 术语解码器 是指解码器筛选器,该筛选器接收压缩视频并输出未压缩的视频。 术语 解码器设备 是指由图形驱动程序实现的硬件视频加速器。
以下是解码器筛选器为支持 DXVA 2.0 而必须执行的基本步骤:
- 协商媒体类型。
- 查找 DXVA 解码器配置。
- 通知视频呈现器解码器正在使用 DXVA 解码。
- 提供用于分配 Direct3D 图面的自定义分配器。
本主题的其余部分更详细地介绍了这些步骤。
迁移说明
如果要从 DXVA 1.0 迁移,应注意这两个版本之间的一些显著差异:
- DXVA 2.0 不使用 IAMVideoAccelerator 和 IAMVideoAcceleratorNotify 接口,因为解码器可以直接通过 IDirectXVideoDecoder 接口访问 DXVA 2.0 API。
- 在媒体类型协商期间,解码器不使用视频加速 GUID 作为子类型。 相反,子类型只是未压缩的视频格式 (,如 NV12) ,与软件解码一样。
- 配置加速器的过程已更改。 在 DXVA 1.0 中,解码器使用DXVA_ConfigPictureDecode结构调用 Execute 来配置 accerlator。 在 DXVA 2.0 中,解码器使用 IDirectXVideoDecoderService 接口,如下一部分所述。
- 解码器分配未压缩的缓冲区。 视频呈现器不再分配它们。
- 解码器不调用 IAMVideoAccelerator::D isplayFrame 来显示解码的帧,而是通过调用 IMemInputPin::Receive 将帧传递给呈现器,就像使用软件解码一样。
- 解码器不再负责检查数据缓冲区何时可以安全地进行更新。 因此,DXVA 2.0 没有任何等效于 IAMVideoAccelerator::QueryRenderStatus 的方法。
- 子图片混合由视频呈现器使用 DXVA2.0 视频处理器 API 完成。 例如,提供子图片的解码器 (,DVD 解码器) 应在单独的输出引脚上发送子图片数据。
对于解码操作,DXVA 2.0 使用与 DXVA 1.0 相同的数据结构。
增强的视频呈现器 (EVR) 筛选器支持 DXVA 2.0。 视频混合呈现器筛选器 (VMR-7 和 VMR-9) 仅支持 DXVA 1.0。
查找解码器配置
解码器协商输出媒体类型后,必须找到 DXVA 解码器设备的兼容配置。 可以在输出引脚的 CBaseOutputPin::CompleteConnect 方法中执行此步骤。 此步骤可确保在解码器提交使用 DXVA 之前,图形驱动程序支持解码器所需的功能。
若要查找解码器设备的配置,请执行以下操作:
查询 IMFGetService 接口的呈现器的输入引脚。
调用 IMFGetService::GetService 以获取指向 IDirect3DDeviceManager9 接口的指针。 服务 GUID 是MR_VIDEO_ACCELERATION_SERVICE。
调用 IDirect3DDeviceManager9::OpenDeviceHandle 以获取呈现器的 Direct3D 设备的句柄。
调用 IDirect3DDeviceManager9::GetVideoService 并传入设备句柄。 此方法返回指向 IDirectXVideoDecoderService 接口的 指针。
调用 IDirectXVideoDecoderService::GetDecoderDeviceGuids。 此方法返回解码器设备 GUID 数组。
循环访问解码器 GUID 数组,以查找解码器筛选器支持的 GUID。 例如,对于 MPEG-2 解码器,你将查找 DXVA2_ModeMPEG2_MOCOMP、 DXVA2_ModeMPEG2_IDCT或 DXVA2_ModeMPEG2_VLD。
找到候选解码器设备 GUID 时,请将 GUID 传递给 IDirectXVideoDecoderService::GetDecoderRenderTargets 方法。 此方法返回呈现目标格式的数组,指定为 D3DFORMAT 值。
循环访问呈现目标格式,并查找与输出格式匹配的格式。 通常,解码器设备支持单个呈现目标格式。 解码器筛选器应使用此子类型连接到呈现器。 在首次调用 CompleteConnect 时,解码器可以确定呈现目标格式,然后将此格式作为首选输出类型返回。
调用 IDirectXVideoDecoderService::GetDecoderConfigurations。 传入相同的解码器设备 GUID 以及描述建议格式 的DXVA2_VideoDesc 结构。 方法返回 DXVA2_ConfigPictureDecode 结构的数组。 每个结构都描述了解码器设备的一种可能配置。
假设前面的步骤成功,请存储 Direct3D 设备句柄、解码器设备 GUID 和配置结构。 筛选器将使用此信息创建解码器设备。
以下代码演示如何查找解码器配置。
HRESULT CDecoder::ConfigureDXVA2(IPin *pPin)
{
UINT cDecoderGuids = 0;
BOOL bFoundDXVA2Configuration = FALSE;
GUID guidDecoder = GUID_NULL;
DXVA2_ConfigPictureDecode config;
ZeroMemory(&config, sizeof(config));
// Variables that follow must be cleaned up at the end.
IMFGetService *pGetService = NULL;
IDirect3DDeviceManager9 *pDeviceManager = NULL;
IDirectXVideoDecoderService *pDecoderService = NULL;
GUID *pDecoderGuids = NULL; // size = cDecoderGuids
HANDLE hDevice = INVALID_HANDLE_VALUE;
// Query the pin for IMFGetService.
HRESULT hr = pPin->QueryInterface(IID_PPV_ARGS(&pGetService));
// Get the Direct3D device manager.
if (SUCCEEDED(hr))
{
hr = pGetService->GetService(
MR_VIDEO_ACCELERATION_SERVICE,
IID_PPV_ARGS(&pDeviceManager)
);
}
// Open a new device handle.
if (SUCCEEDED(hr))
{
hr = pDeviceManager->OpenDeviceHandle(&hDevice);
}
// Get the video decoder service.
if (SUCCEEDED(hr))
{
hr = pDeviceManager->GetVideoService(
hDevice, IID_PPV_ARGS(&pDecoderService));
}
// Get the decoder GUIDs.
if (SUCCEEDED(hr))
{
hr = pDecoderService->GetDecoderDeviceGuids(
&cDecoderGuids, &pDecoderGuids);
}
if (SUCCEEDED(hr))
{
// Look for the decoder GUIDs we want.
for (UINT iGuid = 0; iGuid < cDecoderGuids; iGuid++)
{
// Do we support this mode?
if (!IsSupportedDecoderMode(pDecoderGuids[iGuid]))
{
continue;
}
// Find a configuration that we support.
hr = FindDecoderConfiguration(pDecoderService, pDecoderGuids[iGuid],
&config, &bFoundDXVA2Configuration);
if (FAILED(hr))
{
break;
}
if (bFoundDXVA2Configuration)
{
// Found a good configuration. Save the GUID and exit the loop.
guidDecoder = pDecoderGuids[iGuid];
break;
}
}
}
if (!bFoundDXVA2Configuration)
{
hr = E_FAIL; // Unable to find a configuration.
}
if (SUCCEEDED(hr))
{
// Store the things we will need later.
SafeRelease(&m_pDecoderService);
m_pDecoderService = pDecoderService;
m_pDecoderService->AddRef();
m_DecoderConfig = config;
m_DecoderGuid = guidDecoder;
m_hDevice = hDevice;
}
if (FAILED(hr))
{
if (hDevice != INVALID_HANDLE_VALUE)
{
pDeviceManager->CloseDeviceHandle(hDevice);
}
}
SafeRelease(&pGetService);
SafeRelease(&pDeviceManager);
SafeRelease(&pDecoderService);
return hr;
}
HRESULT CDecoder::FindDecoderConfiguration(
/* [in] */ IDirectXVideoDecoderService *pDecoderService,
/* [in] */ const GUID& guidDecoder,
/* [out] */ DXVA2_ConfigPictureDecode *pSelectedConfig,
/* [out] */ BOOL *pbFoundDXVA2Configuration
)
{
HRESULT hr = S_OK;
UINT cFormats = 0;
UINT cConfigurations = 0;
D3DFORMAT *pFormats = NULL; // size = cFormats
DXVA2_ConfigPictureDecode *pConfig = NULL; // size = cConfigurations
// Find the valid render target formats for this decoder GUID.
hr = pDecoderService->GetDecoderRenderTargets(
guidDecoder,
&cFormats,
&pFormats
);
if (SUCCEEDED(hr))
{
// Look for a format that matches our output format.
for (UINT iFormat = 0; iFormat < cFormats; iFormat++)
{
if (pFormats[iFormat] != (D3DFORMAT)m_fccOutputFormat)
{
continue;
}
// Fill in the video description. Set the width, height, format,
// and frame rate.
DXVA2_VideoDesc videoDesc = {0};
FillInVideoDescription(&videoDesc); // Private helper function.
videoDesc.Format = pFormats[iFormat];
// Get the available configurations.
hr = pDecoderService->GetDecoderConfigurations(
guidDecoder,
&videoDesc,
NULL, // Reserved.
&cConfigurations,
&pConfig
);
if (FAILED(hr))
{
break;
}
// Find a supported configuration.
for (UINT iConfig = 0; iConfig < cConfigurations; iConfig++)
{
if (IsSupportedDecoderConfig(pConfig[iConfig]))
{
// This configuration is good.
*pbFoundDXVA2Configuration = TRUE;
*pSelectedConfig = pConfig[iConfig];
break;
}
}
CoTaskMemFree(pConfig);
break;
} // End of formats loop.
}
CoTaskMemFree(pFormats);
// Note: It is possible to return S_OK without finding a configuration.
return hr;
}
由于此示例是泛型的,因此某些逻辑已放置在需要由解码器实现的帮助程序函数中。 以下代码显示了这些函数的声明:
// Returns TRUE if the decoder supports a given decoding mode.
BOOL IsSupportedDecoderMode(const GUID& mode);
// Returns TRUE if the decoder supports a given decoding configuration.
BOOL IsSupportedDecoderConfig(const DXVA2_ConfigPictureDecode& config);
// Fills in a DXVA2_VideoDesc structure based on the input format.
void FillInVideoDescription(DXVA2_VideoDesc *pDesc);
通知视频呈现器
如果解码器找到解码器配置,下一步是通知视频呈现器解码器将使用硬件加速。 可以在 CompleteConnect 方法中执行此步骤。 此步骤必须在选择分配器之前发生,因为它会影响分配器的选择方式。
- 查询 IMFGetService 接口的呈现器的输入引脚。
- 调用 IMFGetService::GetService 以获取指向 IDirectXVideoMemoryConfiguration 接口的 指针。 服务 GUID 是 MR_VIDEO_ACCELERATION_SERVICE。
- 在循环中调用 IDirectXVideoMemoryConfiguration::GetAvailableSurfaceTypeByIndex ,使 dwTypeIndex 变量从零递增。 当方法返回 pdwType 参数中的值DXVA2_SurfaceType_DecoderRenderTarget时停止。 此步骤可确保视频呈现器支持硬件加速解码。 对于 EVR 筛选器,此步骤将始终成功。
- 如果上一步成功,请使用值DXVA2_SurfaceType_DecoderRenderTarget调用 IDirectXVideoMemoryConfiguration::SetSurfaceType 。 使用此值调用 SetSurfaceType 会使视频呈现器进入 DXVA 模式。 当视频呈现器处于此模式时,解码器必须提供其自己的分配器。
以下代码演示如何通知视频呈现器。
HRESULT CDecoder::SetEVRForDXVA2(IPin *pPin)
{
HRESULT hr = S_OK;
IMFGetService *pGetService = NULL;
IDirectXVideoMemoryConfiguration *pVideoConfig = NULL;
// Query the pin for IMFGetService.
hr = pPin->QueryInterface(__uuidof(IMFGetService), (void**)&pGetService);
// Get the IDirectXVideoMemoryConfiguration interface.
if (SUCCEEDED(hr))
{
hr = pGetService->GetService(
MR_VIDEO_ACCELERATION_SERVICE, IID_PPV_ARGS(&pVideoConfig));
}
// Notify the EVR.
if (SUCCEEDED(hr))
{
DXVA2_SurfaceType surfaceType;
for (DWORD iTypeIndex = 0; ; iTypeIndex++)
{
hr = pVideoConfig->GetAvailableSurfaceTypeByIndex(iTypeIndex, &surfaceType);
if (FAILED(hr))
{
break;
}
if (surfaceType == DXVA2_SurfaceType_DecoderRenderTarget)
{
hr = pVideoConfig->SetSurfaceType(DXVA2_SurfaceType_DecoderRenderTarget);
break;
}
}
}
SafeRelease(&pGetService);
SafeRelease(&pVideoConfig);
return hr;
}
如果解码器找到有效的配置并成功通知视频呈现器,则解码器可以使用 DXVA 进行解码。 解码器必须为其输出引脚实现自定义分配器,如下一部分所述。
分配未压缩的缓冲区
在 DXVA 2.0 中,解码器负责分配 Direct3D 表面以用作未压缩视频缓冲区。 因此,解码器必须实现将创建图面的自定义分配器。 此分配器提供的媒体示例将保存指向 Direct3D 图面的指针。 EVR 通过在媒体示例上调用 IMFGetService::GetService 来检索指向图面的指针。 服务标识符是 MR_BUFFER_SERVICE。
若要提供自定义分配器,请执行以下步骤:
- 为媒体示例定义类。 此类可以从 CMediaSample 类派生。 在此类中,执行以下操作:
- 存储指向 Direct3D 图面的指针。
- 实现 IMFGetService 接口。 在 GetService 方法中,如果服务 GUID MR_BUFFER_SERVICE,请在 Direct3D 图面中查询请求的接口。 否则, GetService 可能会返回 MF_E_UNSUPPORTED_SERVICE。
- 重写 CMediaSample::GetPointer 方法以返回E_NOTIMPL。
- 为分配器定义类。 分配器可以从 CBaseAllocator 类派生。 在此类中,执行以下操作。
- 重写 CBaseAllocator::Alloc 方法。 在此方法中,调用 IDirectXVideoAccelerationService::CreateSurface 来创建图面。 (IDirectXVideoDecoderService 接口从 IDirectXVideoAccelerationService.)
- 重写 CBaseAllocator::Free 方法以释放图面。
- 在筛选器的输出引脚中,重写 CBaseOutputPin::InitAllocator 方法。 在此方法中,创建自定义分配器的实例。
- 在筛选器中,实现 CTransformFilter::D ecideBufferSize 方法。 pProperties 参数指示 EVR 所需的图面数。 将解码器所需的图面数添加到此值中,并在分配器上调用 IMemAllocator::SetProperties 。
以下代码演示如何实现媒体示例类:
class CDecoderSample : public CMediaSample, public IMFGetService
{
friend class CDecoderAllocator;
public:
CDecoderSample(CDecoderAllocator *pAlloc, HRESULT *phr)
: CMediaSample(NAME("DecoderSample"), (CBaseAllocator*)pAlloc, phr, NULL, 0),
m_pSurface(NULL),
m_dwSurfaceId(0)
{
}
// Note: CMediaSample does not derive from CUnknown, so we cannot use the
// DECLARE_IUNKNOWN macro that is used by most of the filter classes.
STDMETHODIMP QueryInterface(REFIID riid, void **ppv)
{
CheckPointer(ppv, E_POINTER);
if (riid == IID_IMFGetService)
{
*ppv = static_cast<IMFGetService*>(this);
AddRef();
return S_OK;
}
else
{
return CMediaSample::QueryInterface(riid, ppv);
}
}
STDMETHODIMP_(ULONG) AddRef()
{
return CMediaSample::AddRef();
}
STDMETHODIMP_(ULONG) Release()
{
// Return a temporary variable for thread safety.
ULONG cRef = CMediaSample::Release();
return cRef;
}
// IMFGetService::GetService
STDMETHODIMP GetService(REFGUID guidService, REFIID riid, LPVOID *ppv)
{
if (guidService != MR_BUFFER_SERVICE)
{
return MF_E_UNSUPPORTED_SERVICE;
}
else if (m_pSurface == NULL)
{
return E_NOINTERFACE;
}
else
{
return m_pSurface->QueryInterface(riid, ppv);
}
}
// Override GetPointer because this class does not manage a system memory buffer.
// The EVR uses the MR_BUFFER_SERVICE service to get the Direct3D surface.
STDMETHODIMP GetPointer(BYTE ** ppBuffer)
{
return E_NOTIMPL;
}
private:
// Sets the pointer to the Direct3D surface.
void SetSurface(DWORD surfaceId, IDirect3DSurface9 *pSurf)
{
SafeRelease(&m_pSurface);
m_pSurface = pSurf;
if (m_pSurface)
{
m_pSurface->AddRef();
}
m_dwSurfaceId = surfaceId;
}
IDirect3DSurface9 *m_pSurface;
DWORD m_dwSurfaceId;
};
以下代码演示如何在分配器上实现 Alloc 方法。
HRESULT CDecoderAllocator::Alloc()
{
CAutoLock lock(this);
HRESULT hr = S_OK;
if (m_pDXVA2Service == NULL)
{
return E_UNEXPECTED;
}
hr = CBaseAllocator::Alloc();
// If the requirements have not changed, do not reallocate.
if (hr == S_FALSE)
{
return S_OK;
}
if (SUCCEEDED(hr))
{
// Free the old resources.
Free();
// Allocate a new array of pointers.
m_ppRTSurfaceArray = new (std::nothrow) IDirect3DSurface9*[m_lCount];
if (m_ppRTSurfaceArray == NULL)
{
hr = E_OUTOFMEMORY;
}
else
{
ZeroMemory(m_ppRTSurfaceArray, sizeof(IDirect3DSurface9*) * m_lCount);
}
}
// Allocate the surfaces.
if (SUCCEEDED(hr))
{
hr = m_pDXVA2Service->CreateSurface(
m_dwWidth,
m_dwHeight,
m_lCount - 1,
(D3DFORMAT)m_dwFormat,
D3DPOOL_DEFAULT,
0,
DXVA2_VideoDecoderRenderTarget,
m_ppRTSurfaceArray,
NULL
);
}
if (SUCCEEDED(hr))
{
for (m_lAllocated = 0; m_lAllocated < m_lCount; m_lAllocated++)
{
CDecoderSample *pSample = new (std::nothrow) CDecoderSample(this, &hr);
if (pSample == NULL)
{
hr = E_OUTOFMEMORY;
break;
}
if (FAILED(hr))
{
break;
}
// Assign the Direct3D surface pointer and the index.
pSample->SetSurface(m_lAllocated, m_ppRTSurfaceArray[m_lAllocated]);
// Add to the sample list.
m_lFree.Add(pSample);
}
}
if (SUCCEEDED(hr))
{
m_bChanged = FALSE;
}
return hr;
}
下面是 Free 方法的代码:
void CDecoderAllocator::Free()
{
CMediaSample *pSample = NULL;
do
{
pSample = m_lFree.RemoveHead();
if (pSample)
{
delete pSample;
}
} while (pSample);
if (m_ppRTSurfaceArray)
{
for (long i = 0; i < m_lAllocated; i++)
{
SafeRelease(&m_ppRTSurfaceArray[i]);
}
delete [] m_ppRTSurfaceArray;
}
m_lAllocated = 0;
}
有关实现自定义分配器的详细信息,请参阅 DirectShow SDK 文档中的主题 提供自定义分配器 。
解码
若要创建解码器设备,请调用 IDirectXVideoDecoderService::CreateVideoDecoder。 方法返回指向解码 器设备的 IDirectXVideoDecoder 接口的指针。
在每个帧上,调用 IDirect3DDeviceManager9::TestDevice 以测试设备句柄。 如果设备已更改,该方法将返回DXVA2_E_NEW_VIDEO_DEVICE。 如果发生这种情况,请执行以下操作:
- 通过调用 IDirect3DDeviceManager9::CloseDeviceHandle 关闭设备句柄。
- 释放 IDirectXVideoDecoderService 和 IDirectXVideoDecoder 指针。
- 打开新的设备句柄。
- 协商新的解码器配置,如 查找解码器配置部分所述。
- 创建新的解码器设备。
假设设备句柄有效,解码过程如下所示:
- 调用 IDirectXVideoDecoder::BeginFrame。
- 执行以下一次或多次操作:
- 调用 IDirectXVideoDecoder::GetBuffer 以获取 DXVA 解码器缓冲区。
- 填充缓冲区。
- 调用 IDirectXVideoDecoder::ReleaseBuffer。
- 调用 IDirectXVideoDecoder::Execute 对帧执行解码操作。
DXVA 2.0 使用与 DXVA 1.0 相同的数据结构进行解码操作。 对于 H.261、H.263 和 MPEG-2) (的原始 DXVA 配置文件集, DXVA 1.0 规范中介绍了这些数据结构。
在每对 BeginFrame/Execute 调用中,可以多次调用 GetBuffer ,但对于每种类型的 DXVA 缓冲区,只能调用一次。 如果使用相同的缓冲区类型调用它两次,将覆盖数据。
调用 Execute 后,调用 IMemInputPin::Receive 将帧传送到视频呈现器,就像软件解码一样。 Receive 方法是异步的;返回后,解码器可以继续解码下一帧。 显示驱动程序可防止任何解码命令在使用缓冲区时覆盖缓冲区。 在呈现器释放示例之前,解码器不应重复使用图面来解码另一帧。 当呈现器释放示例时,分配器将样本放回其可用样本池中。 若要获取下一个可用示例,请调用 CBaseOutputPin::GetDeliveryBuffer,后者又调用 IMemAllocator::GetBuffer。 有关详细信息,请参阅 DirectShow 文档中的主题 DirectShow 中的数据流概述。
相关主题