媒体基础转换解码

Xbox 游戏开发工具包 (GDK) 支持通过媒体基础源阅读器接口对 H264 和 HEVC 文件/数据流进行硬件和软件解码。 与源阅读器的桌面版本相比,Xbox GDK 版本仅支持桌面功能集的一个子集。

支持的编解码器

硬件解码

编解码器 最大分辨率/帧速率 (fps) 档案 主机类型
H264 高达 1080p/30 基线/主要/高 全部 Xbox 主机
HEVC 高达 1080p/30 Main/Main10 Xbox One S、Xbox One X
HEVC 高达 4k/30 Main/Main10 Xbox Series S、Xbox Series X

软件解码

H264 软件解码最高支持 4096x2160@30 fps(5.2 级)。 但是,请注意,性能将取决于主机类型,Xbox Series S/X 能够轻松解码 > 1080p 的分辨率。

还支持对高达 1920x1080@30 fps 的 HEVC 软件解码,其性能取决于主机类型。 但是,我们建议对 HEVC 使用硬件解码,因为与 H264 软件解码相比,它对 CPU 的占用要高得多。

解码媒体数据

使用源阅读器解码媒体数据涉及以下步骤。

  1. 创建源阅读器
  2. 使用硬件(或软件)加速
  3. 枚举输出格式
  4. 设置输出格式
  5. 处理媒体数据
  6. 与图形(或其他 D3D12)命令队列同步
  7. 格式和颜色空间转换

请参阅随 GDK 示例提供的 Mp4Reader 示例以获取参考代码(此处的代码片段来自 Mp4reader,以便更易于理解)。

步骤 1:创建源阅读器

要创建源阅读器实例,请调用 MFCreateSourceReaderFromByteStream 函数,该函数采用指向字节流的指针。 此函数还使用源解析程序创建媒体源。

MFCreateSourceReaderFromByteStream 函数的 pByteStream 参数采用指向 IMFAttributes 接口的指针,该接口用于在源阅读器上设置多个选项(如 IMFAttributes::Set 方法的参考主题中所述)。 将此参数设置为 nullptr 即使用默认行为,但不建议这样做。 要启用硬件加速解码,源阅读器至少应始终指定 MF_SOURCE_READER_D3D_MANAGER 属性。 MFCreateSourceReaderFromByteStream 函数会输出一个指向 IMFSourceReader 接口的指针。

请注意,桌面的源读取器同时支持同步模式和异步模式。 Xbox 游戏开发工具包 (GDK) 仅支持同步模式,这是默认设置。

可以指定 m3u8 或 Smoothstreaming 清单 URL 和用于创建源阅读器的输入以及基本 HLS/Smoothstreaming 支持。 对于 Mp4 文件输入,有一些限制:

  1. AAC-LC 音频和 AC3 是唯一支持的音频格式。
  2. 对于 HEVC 数据,格式标签必须是“hvc1”(不支持“hev1”)。

C++

    // Initialize the Media Foundation platform.
    DX::ThrowIfFailed(MFStartup(MF_VERSION));

    // Call the MFCreateDXGIDeviceManager function to create the Direct3D device manager
    DX::ThrowIfFailed(MFCreateDXGIDeviceManager(&uResetToken, &pDXVAManager));

    // Call the MFResetDXGIDeviceManagerX function with a pointer to the Direct3D device
    DX::ThrowIfFailed(MFResetDXGIDeviceManagerX(pDXVAManager.Get(), device, uResetToken));

    // Create an attribute store
    DX::ThrowIfFailed(pDXVAManager.AsIID(__uuidof(IUnknown), &punkDeviceMgr));
    DX::ThrowIfFailed(MFCreateAttributes(&pMFAttributes, 3));
    DX::ThrowIfFailed(pMFAttributes->SetUnknown(MF_SOURCE_READER_D3D_MANAGER, punkDeviceMgr.Get()));
    DX::ThrowIfFailed(pMFAttributes->SetUINT32(MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS, TRUE));
    DX::ThrowIfFailed(pMFAttributes->SetUINT32(MF_SOURCE_READER_DISABLE_DXVA, FALSE));
    // Don't set the MF_SOURCE_READER_ENABLE_VIDEO_PROCESSING MFAttribute to TRUE. It is too slow.

    // Create the source reader.
    DX::ThrowIfFailed(MFCreateSourceReaderFromURL(INPUT_FILE_PATH, pMFAttributes.Get(), &m_pReader));

步骤2:使用硬件(或软件)加速

源阅读器与 Microsoft DirectX 视频加速 (DXVA) 2.0 兼容,以支持硬件加速视频解码。 要将 DXVA 与源阅读器一起使用,请执行以下步骤。

  1. 调用 MFCreateDXGIDeviceManager 函数以创建 Direct3D 设备管理器。 此函数采用指向 IMFDXGIDeviceManager 接口的指针。
  2. 使用指向 Direct3D 设备的指针调用 MFResetDXGIDeviceManagerX 方法。
  3. 通过调用 MFCreateAttributes 函数创建属性存储。
  4. 调用 SetUnknown,将 MF_SOURCE_READER_D3D_MANAGER 属性设置为 IMFDXGIDeviceManager 接口。
  5. 同时启用 MF_READWRITE_ENABLE_HARDWARE_TRANSFORMS 并将 MF_SOURCE_READER_DISABLE_DXVA 设置为 FALSE。
  6. 创建源阅读器。 传递创建函数的 pAttributes 参数中的属性存储。

软件解码

要启用软件解码,只需将 MF_SOURCE_READER_DISABLE_DXVA 设置为 TRUE。 通过软件解码,可以配置一系列其他参数,如下所示,用于视频解码器上的细粒度线程控制,例如线程关联掩码、工作线程数量和线程优先级别。

C++

    // Set some threading parameters for software decode
    Microsoft::WRL::ComPtr<IMFTransform> transform;
    Microsoft::WRL::ComPtr<IMFAttributes> attributes;
    DX::ThrowIfFailed(m_pReader->GetServiceForStream(MF_SOURCE_READER_FIRST_VIDEO_STREAM, GUID_NULL, IID_PPV_ARGS(transform.GetAddressOf())));
    transform->GetAttributes(attributes.GetAddressOf());
    attributes->SetUINT32(CODECAPI_AVDecVideoThreadAffinityMask, 0x0F); // Use the first 4 cores. Input value is the same as Win32 SetThreadAffinityMask
    attributes->SetUINT32(CODECAPI_AVDecNumWorkerThreads, 0x4); // 4 threads
    attributes->SetUINT32(CODECAPI_AVPriorityControl, THREAD_PRIORITY_HIGHEST); // Set thread priority. Same as Win32 SetThreadPriority for priorty levels

步骤 3:枚举输出格式

每个媒体源都至少有一个流。 例如,视频文件可能包含视频流和音频流。 每个流的格式用媒体类型描述,后者用 IMFMediaType 接口表示。 有关媒体类型的详细信息,请参阅媒体类型。 您必须检查媒体类型以了解您从源阅读器获得的数据的格式。

最初,每个流都有一种默认格式,您可以通过调用 IMFSourceReader::GetCurrentMediaType 方法找到该格式。

对于每个流,媒体源为该流提供可能媒体类型的列表。 类型数目取决于源。 如果源表示一个媒体文件,则每个流通常只有一种类型。 另一方面,网络摄像头可能使用几种不同的格式对视频进行流式处理。 在这种情况下,应用可以从媒体类型列表中选择要使用的格式。

要获取流的某一种媒体类型,请调用 IMFSourceReader::GetNativeMediaType 方法。 此方法有两个索引参数:流的索引和该流的媒体类型列表的索引。 要枚举某个流的所有类型,请在保持流索引不变的同时递增列表索引。 当列表索引超出界限时,GetNativeMediaType 会返回 MF_E_NO_MORE_TYPES,如以下代码所示。

C++

HRESULT EnumerateTypesForStream(IMFSourceReader *pReader, DWORD dwStreamIndex)
{
  HRESULT hr = S_OK;
  DWORD dwMediaTypeIndex = 0;

  while (SUCCEEDED(hr))
  {
    IMFMediaType *pType = NULL;
    hr = pReader->GetNativeMediaType(dwStreamIndex, dwMediaTypeIndex, &pType);
    if (hr == MF_E_NO_MORE_TYPES)
    {
      hr = S_OK;
      break;
    }
    else if (SUCCEEDED(hr))
    {
      // Examine the media type here.

      pType->Release();
    }
    ++dwMediaTypeIndex;
  }
  return hr;
}  

要枚举每个流的媒体类型,请递增流索引。 当流索引超出边界时,IMFSourceReader::GetNativeMediaType 会返回 MF_E_INVALIDSTREAMNUMBER

步骤 4:设置输出格式

要对流进行解码,请创建一个新的媒体类型用来描述所需未压缩格式。 对于解码器,请按如下方式创建媒体类型。

  1. 调用 MFCreateMediaType 以创建新的媒体类型。
  2. 设置 MF_MT_MAJOR_TYPE 属性以指定音频或视频。
  3. 设置 MF_MT_SUBTYPE 属性以指定解码格式的子类型。 若要了解可用的子类型,请参阅音频子类型 GUID视频子类型 GUID。 在 Xbox 上,我们仅支持用于 8 位视频解码的 NV12 和用于 10 位视频解码的 P010。
  4. 调用 SetCurrentMediaType

源阅读器将自动加载解码器。 若要获取解码格式的完整详细信息,请在调用 SetCurrentMediaType 后调用 IMFMediaTypeHandler::GetCurrentMediaType。 以下代码为 NV12 配置视频流,为浮点音频配置音频流。

C++

HRESULT ConfigureDecoder(IMFSourceReader *pReader, DWORD dwStreamIndex)
{
    ComPtr<IMFMediaType> pNativeType;
    ComPtr<IMFMediaType> pType;
    GUID majorType, subtype;

    // Find the native format of the stream.
    DX::ThrowIfFailed(pReader->GetNativeMediaType(dwStreamIndex, 0, &pNativeType));

    // Find the major type.
    DX::ThrowIfFailed(pNativeType->GetGUID(MF_MT_MAJOR_TYPE, &majorType));

    // Define the output type.
    DX::ThrowIfFailed(MFCreateMediaType(&pType));

    DX::ThrowIfFailed(pType->SetGUID(MF_MT_MAJOR_TYPE, majorType));

    // Select a subtype.
    if (majorType == MFMediaType_Video)
    {
        // NV12 for 8 bit (or P010 for 10 bit)are the only supported output types of Xbox One HW decoders
        subtype = MFVideoFormat_NV12;
    }
    else if (majorType == MFMediaType_Audio)
    {
        subtype = MFAudioFormat_Float;
    }
    else
    {
        // Unrecognized type. Skip.
        return;
    }

    DX::ThrowIfFailed(pType->SetGUID(MF_MT_SUBTYPE, subtype));

    // Set the uncompressed format.
    DX::ThrowIfFailed(pReader->SetCurrentMediaType(dwStreamIndex, nullptr, pType.Get()));
}  

步骤 5:处理媒体数据

要从源获取媒体数据,请调用 IMFSourceReader::ReadSample 方法,如以下代码所示。

C++

DWORD streamIndex, flags;
LONGLONG llTimeStamp;

hr = pReader->ReadSample(
  MF_SOURCE_READER_ANY_STREAM,    // Stream index.
  0,                              // Flags.
  &streamIndex,                   // Receives the actual stream index.
  &flags,                         // Receives status flags.
  &llTimeStamp,                   // Receives the time stamp.
  &pSample                        // Receives the sample or nullptr.
  );  

第一个参数是你想要获取其数据的流的索引。 你还可指定 MF_SOURCE_READER_ANY_STREAM,从任何流获取下一可用数据。 第二个参数包含可选标志。 有关这些标志的列表,请参阅 MF_SOURCE_READER_CONTROL_FLAG。 第三个参数接收实际生成数据的流的索引。 如果将第一个参数设置为 MF_SOURCE_READER_ANY_STREAM,则需要此信息。 第四个参数接收状态标志,它们指示在读取数据时可能出现的各种事件,比如流中格式出现更改。 有关状态标志的列表,请参阅 MF_SOURCE_READER_FLAG

如果媒体源能够为请求的流生成数据,则 ReadSample 的最后一个参数将收到指向媒体样本对象的 IMFSample 接口的指针。 使用媒体样本:

  • 获取指向媒体数据的指针。
  • 获取演示时间和样本持续时间。
  • 获取描述样本的交错、场控制和其他方面的属性。

媒体数据的内容取决于流的格式。 对于未压缩的视频流,每个媒体样本都包含一个视频帧。 对于未压缩的音频流,每个媒体样本都包含一个音频帧序列。

ReadSample 方法可返回 S_OK,但尚不返回 pSample 参数中的媒体样本。 例如,当到达文件末尾时,ReadSample 会在 dwFlags 中设置 MF_SOURCE_READERF_ENDOFSTREAM 标志,并将 pSample 设置为 nullptr。 在此情况下,即使 pSample 参数设置为 nullptrReadSample 方法也会返回 S_OK,因为未发生错误。 因此,在取消引用 pSample 的值之前,始终检查该值。

在调用 ReadSample 之后,视频示例需要一些额外处理。 这包括根据 MF_MT_FRAME_SIZEMF_MT_MINIMUM_DISPLAY_APERTURE 属性确定视频宽度和高度以及处理同步。

以下代码片段调用 ReadSample,然后检查第一个视频流的方法返回的信息。 有关如何处理音频流数据,请参阅 Mp4reader 示例。 下一节将介绍硬件视频解码操作所需的同步。

C++

  // Retreive sample from source reader
    ComPtr<IMFSample> pOutputSample;

    hr = m_pReader->ReadSample(
        uint32_t(MF_SOURCE_READER_FIRST_VIDEO_STREAM), // Stream index.
        0,                                             // Flags.
        &streamIndex,                                  // Receives the actual stream index. 
        &dwStreamFlags,                                // Receives status flags.
        &llTimestamp,                                  // Receives the time stamp.
        &pOutputSample                                 // Receives the sample or nullptr. If this parameter receives a non-NULL pointer, the caller must release the  
                                                        // interface.
    );

    if (SUCCEEDED(hr))
    {
        if (dwStreamFlags & MF_SOURCE_READERF_ENDOFSTREAM)
        {
            m_videoDone = true;
        }

        if (dwStreamFlags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED)
        {
            // The format changed. Reconfigure the decoder.
            ConfigureSourceReaderOutput(m_pReader.Get(), streamIndex);
        }

        if (pOutputSample)
        {
            if (m_videoWidth == 0 || m_videoHeight == 0
                || (dwStreamFlags & MF_SOURCE_READERF_NATIVEMEDIATYPECHANGED) || (dwStreamFlags & MF_SOURCE_READERF_CURRENTMEDIATYPECHANGED))
            {
                // Update video width and height
                ComPtr<IMFMediaType> pMediaType;

                if (SUCCEEDED(m_pReader->GetCurrentMediaType(uint32_t(MF_SOURCE_READER_FIRST_VIDEO_STREAM), &pMediaType)))
                {
                    MFVideoArea videoArea = {};
                    if (SUCCEEDED(pMediaType->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, (uint8_t*)&videoArea, sizeof(MFVideoArea), nullptr)))
                    {
                        m_videoWidth = UINT(videoArea.Area.cx);
                        m_videoHeight = UINT(videoArea.Area.cy);
                    }
                    else
                    {
                        DX::ThrowIfFailed(MFGetAttributeSize(pMediaType.Get(), MF_MT_FRAME_SIZE, &m_videoWidth, &m_videoHeight));
                    }
                }
            }

            if (m_pOutputVideoSample)
            {
                DX::ThrowIfFailed(MFD3D12GpuSignalSampleFree(m_pVideoRender->GetVideoProcessCommandQueue(), m_pOutputVideoSample.Get()));
                m_pOutputVideoSample.Reset();
            }

            // The output buffer may still used by decoding ( although decode returns the buffer from CPU), put a wait single on the GPU to wait to the decoding to complete
            DX::ThrowIfFailed(MFD3D12GpuWaitForSampleReady(m_pVideoRender->GetVideoProcessCommandQueue(), pOutputSample.Get()));

            m_pOutputVideoSample = pOutputSample;
            ++m_numberOfFramesDecoded;
        }
    }

通过 IMFSourceReader::SetCurrentPosition API 支持搜索。

C++

    PROPVARIANT position;
    position.vt = VT_I8;
    position.hVal.QuadPart = hnsPosition;
    m_pReader->SetCurrentPosition(GUID_NULL, position);

步骤 6:与图形(或其他 D3D12 命令队列)同步

在 Xbox GDK 中,由调用者确保来自硬件解码器的视频示例与图形或其他 D3D12 队列正确同步。 有 2 个新的 API 可以帮助解决这个问题:

  1. EnqueueResourceReadyWait将在传入的命令队列中添加一个等待,以确保该操作在处理示例之前等待解码完成。 这必须在 ReadSample 之后和其他队列开始处理视频示例之前立即调用。
  2. EnqueueResourceRelease将确保解码器等待传入命令队列中的任何挂起操作(它将要求命令队列发出内部围栏的信号)。 必须在其他命令队列上的所有处理完成后调用此命令,以将示例返回到池中以供重复使用。

C++

    // For D3D12, application must call MFD3D12GpuSignalForSampleFree after it has finished processing the video sample
    HRESULT MFD3D12GpuSignalSampleFree(
        _In_ ID3D12CommandQueue* pCmdQueue,
        _In_ IMFSample* pVideoSample)
    {       
        ComPtr<IMFMediaBuffer> pBuffer;
        HRESULT hr = pVideoSample->GetBufferByIndex(0, &pBuffer);
        if (SUCCEEDED(hr))
        {
            ComPtr<IMFDXGIBuffer>  pDXGIBuffer;
            hr = pBuffer->QueryInterface<IMFDXGIBuffer>(&pDXGIBuffer);
            if (SUCCEEDED(hr))
            {
                ComPtr<IMFD3D12SynchronizationObjectCommands> pMFSyncObj;
                hr = pDXGIBuffer->GetUnknown(MF_D3D12_SYNCHRONIZATION_OBJECT, IID_PPV_ARGS(&pMFSyncObj));
                if (SUCCEEDED(hr))
                {
                    //GPU signal the sample can be freed for decoding
                    hr = pMFSyncObj->EnqueueResourceRelease(pCmdQueue);
                }
            }
        }
        return hr;
    }

    // For D3D12, application must call MFD3D12GpuWaitForSampleReady to make sure the GPU waits for decode to complete before process the sample using GPU code
    HRESULT MFD3D12GpuWaitForSampleReady(
        _In_ ID3D12CommandQueue* pCmdQueue,
        _In_ IMFSample* pVideoSample)
    {
        ComPtr<IMFMediaBuffer> pBuffer;
        HRESULT hr = pVideoSample->GetBufferByIndex(0, &pBuffer);
        if (SUCCEEDED(hr))
        {
            ComPtr<IMFDXGIBuffer> pDXGIBuffer;
            hr = pBuffer->QueryInterface<IMFDXGIBuffer>(&pDXGIBuffer);
            if (SUCCEEDED(hr))
            {
                ComPtr<IMFD3D12SynchronizationObjectCommands> pMFSyncObj;
                hr = pDXGIBuffer->GetUnknown(MF_D3D12_SYNCHRONIZATION_OBJECT, IID_PPV_ARGS(&pMFSyncObj));
                if (SUCCEEDED(hr))
                {
                    // GPU wait until the decoding completed
                    hr = pMFSyncObj->EnqueueResourceReadyWait(pCmdQueue);
                }
            }
        }
        return hr;
    }

软件解码不需要这些同步调用。 从 ReadSample 返回的示例将始终具有完整的解码帧。

第 7 步:格式和颜色空间转换

现在可以使用 D3D12 视频处理器 API 执行解码输出的颜色空间转换和格式转换。 Xbox GDK 仅支持 YUV (DXGI_FORMAT_NV12/DXGI_FORMAT_P010) 到 RGB (DXGI_FORMAT_R10G10B10A2_UNORM/DXGI_FORMAT_R8G8B8A8_UNORM 包括其他 8 位 RGB/无类型变体) 的格式转换。

除了 YUV-RGB 格式转换外,如果需要,还可以使用视频处理器同时进行颜色空间转换。 支持的转换包括:

  1. BT.2020 至 BT.709
  2. BT.709 至 BT.2020

有关适当的颜色空间类型,请参阅 DXGI_COLOR_SPACE_TYPE 枚举。

创建视频处理器

调用 ID3D12VideoDevice::CreateVideoProcessor 创建 ID3D12VideoProcessor 的实例。 视频处理器保持视频处理会话的状态,包括所需的中间内存、缓存的处理数据或其他临时工作空间。 视频处理器创建参数指定在 ID3D12VideoProcessCommandList1::ProcessFrames 时间执行或可用的操作。

C++

    D3D12_VIDEO_PROCESS_INPUT_STREAM_DESC inputStreamDesc{};
    inputStreamDesc.Format = DXGI_FORMAT_NV12;
    inputStreamDesc.ColorSpace = DXGI_COLOR_SPACE_YCBCR_STUDIO_G22_LEFT_P709;
    inputStreamDesc.SourceSizeRange = D3D12_VIDEO_SIZE_RANGE{ g_MaxVideoWidth, g_MaxVideoHeight, 1, 1 };
    inputStreamDesc.DestinationSizeRange = D3D12_VIDEO_SIZE_RANGE{ g_MaxVideoWidth, g_MaxVideoHeight, 1, 1 };

    D3D12_VIDEO_PROCESS_OUTPUT_STREAM_DESC outputStreamDesc{};
    outputStreamDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
    outputStreamDesc.ColorSpace = DXGI_COLOR_SPACE_RGB_STUDIO_G22_NONE_P709;

    hr = pVideoDevice->CreateVideoProcessor(0, &outputStreamDesc, 1, &inputStreamDesc, IID_GRAPHICS_PPV_ARGS(m_pVideoProcessor.GetAddressOf()));

视频处理器可用于记录来自多个命令列表的命令,但一次只能与一个命令列表关联。 应用程序负责同步访问视频处理器。 应用程序还必须按照在 GPU 上执行的顺序来记录针对视频处理器的视频处理命令。

从 IMFSample 查询输出纹理

以下代码片段显示了如何从 IMFSample 查询解码的 D3D12 输出纹理以用于视频处理或其他图形操作

C++

    IMFSample* pSample = pOutputDecodedSample;
    HRESULT hr = pSample->GetBufferCount(&bufferCount);
    if (FAILED(hr))
        return hr;
    assert(bufferCount == 1);

    ComPtr<IMFMediaBuffer> spBuffer;
    hr = pSample->GetBufferByIndex(0, spBuffer.GetAddressOf());
    if (FAILED(hr))
        return hr;

    ComPtr<IMFDXGIBuffer> spDXGIBuffer;
    hr = spBuffer.Get()->QueryInterface(spDXGIBuffer.GetAddressOf());
    if (FAILED(hr))
        return hr;

    ComPtr<ID3D12Resource> spResourceTexture;
    hr = spDXGIBuffer->GetResource(IID_GRAPHICS_PPV_ARGS(spResourceTexture.GetAddressOf()));
    if (FAILED(hr))
        return hr;

    UINT32 uiIndexSrc;
    hr = spDXGIBuffer->GetSubresourceIndex(&uiIndexSrc);
    if (FAILED(hr))
        return hr;

视频处理器命令执行

创建和执行视频处理器命令使用标准 D3D12 工作提交模型,使用 D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS 命令列表/队列/分配器。 以下示例代码显示了如何创建它们:

C++

HRESULT hr = pDevice->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS, IID_GRAPHICS_PPV_ARGS(m_pVpCommandAllocator.GetAddressOf()));
    if (FAILED(hr))
        return hr;

    D3D12_COMMAND_QUEUE_DESC descQueue{};
    descQueue.Type = D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS;
    descQueue.Priority = 0;
    descQueue.Flags = D3D12_COMMAND_QUEUE_FLAG_NONE;

    hr = pDevice->CreateCommandQueue(&descQueue, IID_GRAPHICS_PPV_ARGS(m_pVpCommandQueue.GetAddressOf()));
    if (FAILED(hr))
        return hr;

    hr = pDevice->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_VIDEO_PROCESS, m_pVpCommandAllocator.Get(), nullptr, __uuidof(ID3D12CommandList), reinterpret_cast<void**>(m_pVpCommandList.GetAddressOf()));
    if (FAILED(hr))
        return hr;

视频处理操作的所有输入和输出参数都组织成一个输入参数结构 D3D12_VIDEO_PROCESS_INPUT_STREAM_ARGUMENTS 和一个输出参数结构 D3D12_VIDEO_PROCESS_OUTPUT_STREAM_ARGUMENTS。 应用程序必须调用 ID3D12VideoProcessCommandList::ProcessFrames 来记录它要执行的视频处理操作。

记录命令列表后,在视频处理器命令队列上调用 ID3D12CommandQueue::ExecuteCommandLists,将帧处理提交给 GPU。 注意:在 Xbox Series S/X 上,需要同步对视频处理器命令队列和图形命令队列的访问,因为多线程访问可能会导致意外行为。