在 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 而必须执行的基本步骤:

  1. 协商媒体类型。
  2. 查找 DXVA 解码器配置。
  3. 通知视频呈现器解码器正在使用 DXVA 解码。
  4. 提供用于分配 Direct3D 图面的自定义分配器。

本主题的其余部分更详细地介绍了这些步骤。

迁移说明

如果要从 DXVA 1.0 迁移,应注意这两个版本之间的一些显著差异:

  • DXVA 2.0 不使用 IAMVideoAcceleratorIAMVideoAcceleratorNotify 接口,因为解码器可以直接通过 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 之前,图形驱动程序支持解码器所需的功能。

若要查找解码器设备的配置,请执行以下操作:

  1. 查询 IMFGetService 接口的呈现器的输入引脚。

  2. 调用 IMFGetService::GetService 以获取指向 IDirect3DDeviceManager9 接口的指针。 服务 GUID 是MR_VIDEO_ACCELERATION_SERVICE。

  3. 调用 IDirect3DDeviceManager9::OpenDeviceHandle 以获取呈现器的 Direct3D 设备的句柄。

  4. 调用 IDirect3DDeviceManager9::GetVideoService 并传入设备句柄。 此方法返回指向 IDirectXVideoDecoderService 接口的 指针。

  5. 调用 IDirectXVideoDecoderService::GetDecoderDeviceGuids。 此方法返回解码器设备 GUID 数组。

  6. 循环访问解码器 GUID 数组,以查找解码器筛选器支持的 GUID。 例如,对于 MPEG-2 解码器,你将查找 DXVA2_ModeMPEG2_MOCOMPDXVA2_ModeMPEG2_IDCTDXVA2_ModeMPEG2_VLD

  7. 找到候选解码器设备 GUID 时,请将 GUID 传递给 IDirectXVideoDecoderService::GetDecoderRenderTargets 方法。 此方法返回呈现目标格式的数组,指定为 D3DFORMAT 值。

  8. 循环访问呈现目标格式,并查找与输出格式匹配的格式。 通常,解码器设备支持单个呈现目标格式。 解码器筛选器应使用此子类型连接到呈现器。 在首次调用 CompleteConnect 时,解码器可以确定呈现目标格式,然后将此格式作为首选输出类型返回。

  9. 调用 IDirectXVideoDecoderService::GetDecoderConfigurations。 传入相同的解码器设备 GUID 以及描述建议格式 的DXVA2_VideoDesc 结构。 方法返回 DXVA2_ConfigPictureDecode 结构的数组。 每个结构都描述了解码器设备的一种可能配置。

  10. 假设前面的步骤成功,请存储 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 方法中执行此步骤。 此步骤必须在选择分配器之前发生,因为它会影响分配器的选择方式。

  1. 查询 IMFGetService 接口的呈现器的输入引脚。
  2. 调用 IMFGetService::GetService 以获取指向 IDirectXVideoMemoryConfiguration 接口的 指针。 服务 GUID 是 MR_VIDEO_ACCELERATION_SERVICE
  3. 在循环中调用 IDirectXVideoMemoryConfiguration::GetAvailableSurfaceTypeByIndex ,使 dwTypeIndex 变量从零递增。 当方法返回 pdwType 参数中的值DXVA2_SurfaceType_DecoderRenderTarget时停止。 此步骤可确保视频呈现器支持硬件加速解码。 对于 EVR 筛选器,此步骤将始终成功。
  4. 如果上一步成功,请使用值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

若要提供自定义分配器,请执行以下步骤:

  1. 为媒体示例定义类。 此类可以从 CMediaSample 类派生。 在此类中,执行以下操作:
    • 存储指向 Direct3D 图面的指针。
    • 实现 IMFGetService 接口。 在 GetService 方法中,如果服务 GUID MR_BUFFER_SERVICE,请在 Direct3D 图面中查询请求的接口。 否则, GetService 可能会返回 MF_E_UNSUPPORTED_SERVICE
    • 重写 CMediaSample::GetPointer 方法以返回E_NOTIMPL。
  2. 为分配器定义类。 分配器可以从 CBaseAllocator 类派生。 在此类中,执行以下操作。
  3. 在筛选器的输出引脚中,重写 CBaseOutputPin::InitAllocator 方法。 在此方法中,创建自定义分配器的实例。
  4. 在筛选器中,实现 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。 如果发生这种情况,请执行以下操作:

  1. 通过调用 IDirect3DDeviceManager9::CloseDeviceHandle 关闭设备句柄
  2. 释放 IDirectXVideoDecoderServiceIDirectXVideoDecoder 指针。
  3. 打开新的设备句柄。
  4. 协商新的解码器配置,如 查找解码器配置部分所述。
  5. 创建新的解码器设备。

假设设备句柄有效,解码过程如下所示:

  1. 调用 IDirectXVideoDecoder::BeginFrame
  2. 执行以下一次或多次操作:
    1. 调用 IDirectXVideoDecoder::GetBuffer 以获取 DXVA 解码器缓冲区。
    2. 填充缓冲区。
    3. 调用 IDirectXVideoDecoder::ReleaseBuffer
  3. 调用 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 中的数据流概述

DirectX 视频加速 2.0