Konversi warna dari DXGI_FORMAT_B8G8R8A8_UNORM ke NV12 dalam GPU menggunakan DirectX11 pixel shaders

9

Saya sedang mengerjakan kode untuk menangkap desktop menggunakan duplikasi Desktop dan menyandikan yang sama ke h264 menggunakan Intel hardwareMFT. Encoder hanya menerima format NV12 sebagai input. Saya telah mendapatkan DXGI_FORMAT_B8G8R8A8_UNORM ke NV12 converter ( https://github.com/NVIDIA/video-sdk-samples/blob/master/nvEncDXGIOutputDuplicationSample/Preproc.cpp ) yang berfungsi dengan baik, dan didasarkan pada DirectX VideoProcessor.

Masalahnya adalah bahwa VideoProcessor pada perangkat keras grafis intel tertentu hanya mendukung konversi dari DXGI_FORMAT_B8G8R8A8_UNORM ke YUY2 tetapi tidak NV12, saya telah mengkonfirmasi hal yang sama dengan menyebutkan format yang didukung melalui GetVideoProcessorOutputFormats. Meskipun VideoProcessor Blt berhasil tanpa kesalahan, dan saya bisa melihat bahwa frame dalam video output sedikit pixelated, saya bisa memperhatikannya jika saya melihatnya dengan seksama.

Saya kira, VideoProcessor telah gagal ke format output yang didukung berikutnya (YUY2) dan saya tanpa sadar memasukkannya ke encoder yang berpikir bahwa input dalam NV12 telah dikonfigurasi. Tidak ada kegagalan atau kerusakan besar pada frame karena fakta bahwa ada sedikit perbedaan seperti urutan byte dan subsampling antara NV12 dan YUY2. Juga, saya tidak memiliki masalah pixelating pada perangkat keras yang mendukung konversi NV12.

Jadi saya memutuskan untuk melakukan konversi warna menggunakan pixel shaders yang didasarkan pada kode ini ( https://github.com/bavulapati/DXGICaptureDXColorSpaceConversionIntelEncode/blob/master/DXGICaptureDXColorSpaceConversionIntelEncode/DuplicationManager.cpp ). Saya dapat membuat pixel shaders berfungsi, saya juga telah mengunggah kode saya di sini ( https://codeshare.io/5PJjxP ) untuk referensi (sedapat mungkin disederhanakan).

Sekarang, saya masing-masing memiliki dua saluran, chroma, dan luma (tekstur ID3D11Texture2D). Dan saya benar-benar bingung tentang mengemas secara efisien dua saluran terpisah menjadi satu tekstur ID3D11Texture2D sehingga saya dapat memasukkan hal yang sama ke pembuat enkode. Apakah ada cara untuk mengemas saluran Y dan UV secara efisien ke dalam ID3D11Texture2D tunggal dalam GPU? Saya benar-benar bosan dengan pendekatan berbasis CPU karena fakta bahwa itu mahal, dan tidak menawarkan frame rate sebaik mungkin. Bahkan, saya enggan menyalin tekstur ke CPU. Saya sedang memikirkan cara untuk melakukannya dalam GPU tanpa salinan bolak-balik antara CPU dan GPU.

Saya telah meneliti ini selama beberapa waktu tanpa kemajuan, bantuan apa pun akan dihargai.

/**
* This method is incomplete. It's just a template of what I want to achieve.
*/

HRESULT CreateNV12TextureFromLumaAndChromaSurface(ID3D11Texture2D** pOutputTexture)
{
    HRESULT hr = S_OK;

    try
    {
        //Copying from GPU to CPU. Bad :(
        m_pD3D11DeviceContext->CopyResource(m_CPUAccessibleLuminanceSurf, m_LuminanceSurf);

        D3D11_MAPPED_SUBRESOURCE resource;
        UINT subresource = D3D11CalcSubresource(0, 0, 0);

        HRESULT hr = m_pD3D11DeviceContext->Map(m_CPUAccessibleLuminanceSurf, subresource, D3D11_MAP_READ, 0, &resource);

        BYTE* sptr = reinterpret_cast<BYTE*>(resource.pData);
        BYTE* dptrY = nullptr; // point to the address of Y channel in output surface

        //Store Image Pitch
        int m_ImagePitch = resource.RowPitch;

        int height = GetImageHeight();
        int width = GetImageWidth();

        for (int i = 0; i < height; i++)
        {
            memcpy_s(dptrY, m_ImagePitch, sptr, m_ImagePitch);

            sptr += m_ImagePitch;
            dptrY += m_ImagePitch;
        }

        m_pD3D11DeviceContext->Unmap(m_CPUAccessibleLuminanceSurf, subresource);

        //Copying from GPU to CPU. Bad :(
        m_pD3D11DeviceContext->CopyResource(m_CPUAccessibleChrominanceSurf, m_ChrominanceSurf);
        hr = m_pD3D11DeviceContext->Map(m_CPUAccessibleChrominanceSurf, subresource, D3D11_MAP_READ, 0, &resource);

        sptr = reinterpret_cast<BYTE*>(resource.pData);
        BYTE* dptrUV = nullptr; // point to the address of UV channel in output surface

        m_ImagePitch = resource.RowPitch;
        height /= 2;
        width /= 2;

        for (int i = 0; i < height; i++)
        {
            memcpy_s(dptrUV, m_ImagePitch, sptr, m_ImagePitch);

            sptr += m_ImagePitch;
            dptrUV += m_ImagePitch;
        }

        m_pD3D11DeviceContext->Unmap(m_CPUAccessibleChrominanceSurf, subresource);
    }
    catch(HRESULT){}

    return hr;
}

Draw NV12:

 //
// Draw frame for NV12 texture
//
HRESULT DrawNV12Frame(ID3D11Texture2D* inputTexture)
{
    HRESULT hr;

    // If window was resized, resize swapchain
    if (!m_bIntialized)
    {
        HRESULT Ret = InitializeNV12Surfaces(inputTexture);
        if (!SUCCEEDED(Ret))
        {
            return Ret;
        }

        m_bIntialized = true;
    }

    m_pD3D11DeviceContext->CopyResource(m_ShaderResourceSurf, inputTexture);

    D3D11_TEXTURE2D_DESC FrameDesc;
    m_ShaderResourceSurf->GetDesc(&FrameDesc);

    D3D11_SHADER_RESOURCE_VIEW_DESC ShaderDesc;
    ShaderDesc.Format = FrameDesc.Format;
    ShaderDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
    ShaderDesc.Texture2D.MostDetailedMip = FrameDesc.MipLevels - 1;
    ShaderDesc.Texture2D.MipLevels = FrameDesc.MipLevels;

    // Create new shader resource view
    ID3D11ShaderResourceView* ShaderResource = nullptr;
    hr = m_pD3D11Device->CreateShaderResourceView(m_ShaderResourceSurf, &ShaderDesc, &ShaderResource);

    IF_FAILED_THROW(hr);

    m_pD3D11DeviceContext->PSSetShaderResources(0, 1, &ShaderResource);

    // Set resources
    m_pD3D11DeviceContext->OMSetRenderTargets(1, &m_pLumaRT, nullptr);
    m_pD3D11DeviceContext->PSSetShader(m_pPixelShaderLuma, nullptr, 0);
    m_pD3D11DeviceContext->RSSetViewports(1, &m_VPLuminance);

    // Draw textured quad onto render target
    m_pD3D11DeviceContext->Draw(NUMVERTICES, 0);

    m_pD3D11DeviceContext->OMSetRenderTargets(1, &m_pChromaRT, nullptr);
    m_pD3D11DeviceContext->PSSetShader(m_pPixelShaderChroma, nullptr, 0);
    m_pD3D11DeviceContext->RSSetViewports(1, &m_VPChrominance);

    // Draw textured quad onto render target
    m_pD3D11DeviceContext->Draw(NUMVERTICES, 0);

    // Release shader resource
    ShaderResource->Release();
    ShaderResource = nullptr;

    return S_OK;
}

Init shaders:

void SetViewPort(D3D11_VIEWPORT* VP, UINT Width, UINT Height)
{
    VP->Width = static_cast<FLOAT>(Width);
    VP->Height = static_cast<FLOAT>(Height);
    VP->MinDepth = 0.0f;
    VP->MaxDepth = 1.0f;
    VP->TopLeftX = 0;
    VP->TopLeftY = 0;
}

HRESULT MakeRTV(ID3D11RenderTargetView** pRTV, ID3D11Texture2D* pSurf)
{
    if (*pRTV)
    {
        (*pRTV)->Release();
        *pRTV = nullptr;
    }
    // Create a render target view
    HRESULT hr = m_pD3D11Device->CreateRenderTargetView(pSurf, nullptr, pRTV);

    IF_FAILED_THROW(hr);

    return S_OK;
}

HRESULT InitializeNV12Surfaces(ID3D11Texture2D* inputTexture)
{
    ReleaseSurfaces();

    D3D11_TEXTURE2D_DESC lOutputDuplDesc;
    inputTexture->GetDesc(&lOutputDuplDesc);


    // Create shared texture for all duplication threads to draw into
    D3D11_TEXTURE2D_DESC DeskTexD;
    RtlZeroMemory(&DeskTexD, sizeof(D3D11_TEXTURE2D_DESC));
    DeskTexD.Width = lOutputDuplDesc.Width;
    DeskTexD.Height = lOutputDuplDesc.Height;
    DeskTexD.MipLevels = 1;
    DeskTexD.ArraySize = 1;
    DeskTexD.Format = lOutputDuplDesc.Format;
    DeskTexD.SampleDesc.Count = 1;
    DeskTexD.Usage = D3D11_USAGE_DEFAULT;
    DeskTexD.BindFlags = D3D11_BIND_SHADER_RESOURCE;

    HRESULT hr = m_pD3D11Device->CreateTexture2D(&DeskTexD, nullptr, &m_ShaderResourceSurf);
    IF_FAILED_THROW(hr);

    DeskTexD.Format = DXGI_FORMAT_R8_UNORM;
    DeskTexD.BindFlags = D3D11_BIND_RENDER_TARGET;

    hr = m_pD3D11Device->CreateTexture2D(&DeskTexD, nullptr, &m_LuminanceSurf);
    IF_FAILED_THROW(hr);

    DeskTexD.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    DeskTexD.Usage = D3D11_USAGE_STAGING;
    DeskTexD.BindFlags = 0;

    hr = m_pD3D11Device->CreateTexture2D(&DeskTexD, NULL, &m_CPUAccessibleLuminanceSurf);
    IF_FAILED_THROW(hr);

    SetViewPort(&m_VPLuminance, DeskTexD.Width, DeskTexD.Height);

    HRESULT Ret = MakeRTV(&m_pLumaRT, m_LuminanceSurf);
    if (!SUCCEEDED(Ret))
        return Ret;

    DeskTexD.Width = lOutputDuplDesc.Width / 2;
    DeskTexD.Height = lOutputDuplDesc.Height / 2;
    DeskTexD.Format = DXGI_FORMAT_R8G8_UNORM;

    DeskTexD.Usage = D3D11_USAGE_DEFAULT;
    DeskTexD.CPUAccessFlags = 0;
    DeskTexD.BindFlags = D3D11_BIND_RENDER_TARGET;

    hr = m_pD3D11Device->CreateTexture2D(&DeskTexD, nullptr, &m_ChrominanceSurf);
    IF_FAILED_THROW(hr);

    DeskTexD.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    DeskTexD.Usage = D3D11_USAGE_STAGING;
    DeskTexD.BindFlags = 0;

    hr = m_pD3D11Device->CreateTexture2D(&DeskTexD, NULL, &m_CPUAccessibleChrominanceSurf);
    IF_FAILED_THROW(hr);

    SetViewPort(&m_VPChrominance, DeskTexD.Width, DeskTexD.Height);
    return MakeRTV(&m_pChromaRT, m_ChrominanceSurf);
}

HRESULT InitVertexShader(ID3D11VertexShader** ppID3D11VertexShader)
{
    HRESULT hr = S_OK;
    UINT Size = ARRAYSIZE(g_VS);

    try
    {
        IF_FAILED_THROW(m_pD3D11Device->CreateVertexShader(g_VS, Size, NULL, ppID3D11VertexShader));;

        m_pD3D11DeviceContext->VSSetShader(m_pVertexShader, nullptr, 0);

        // Vertices for drawing whole texture
        VERTEX Vertices[NUMVERTICES] =
        {
            { XMFLOAT3(-1.0f, -1.0f, 0), XMFLOAT2(0.0f, 1.0f) },
            { XMFLOAT3(-1.0f, 1.0f, 0), XMFLOAT2(0.0f, 0.0f) },
            { XMFLOAT3(1.0f, -1.0f, 0), XMFLOAT2(1.0f, 1.0f) },
            { XMFLOAT3(1.0f, -1.0f, 0), XMFLOAT2(1.0f, 1.0f) },
            { XMFLOAT3(-1.0f, 1.0f, 0), XMFLOAT2(0.0f, 0.0f) },
            { XMFLOAT3(1.0f, 1.0f, 0), XMFLOAT2(1.0f, 0.0f) },
        };

        UINT Stride = sizeof(VERTEX);
        UINT Offset = 0;

        D3D11_BUFFER_DESC BufferDesc;
        RtlZeroMemory(&BufferDesc, sizeof(BufferDesc));
        BufferDesc.Usage = D3D11_USAGE_DEFAULT;
        BufferDesc.ByteWidth = sizeof(VERTEX) * NUMVERTICES;
        BufferDesc.BindFlags = D3D11_BIND_VERTEX_BUFFER;
        BufferDesc.CPUAccessFlags = 0;
        D3D11_SUBRESOURCE_DATA InitData;
        RtlZeroMemory(&InitData, sizeof(InitData));
        InitData.pSysMem = Vertices;

        // Create vertex buffer
        IF_FAILED_THROW(m_pD3D11Device->CreateBuffer(&BufferDesc, &InitData, &m_VertexBuffer));

        m_pD3D11DeviceContext->IASetVertexBuffers(0, 1, &m_VertexBuffer, &Stride, &Offset);
        m_pD3D11DeviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

        D3D11_INPUT_ELEMENT_DESC Layout[] =
        {
            { "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
            { "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 }
        };

        UINT NumElements = ARRAYSIZE(Layout);
        hr = m_pD3D11Device->CreateInputLayout(Layout, NumElements, g_VS, Size, &m_pVertexLayout);

        m_pD3D11DeviceContext->IASetInputLayout(m_pVertexLayout);
    }
    catch (HRESULT) {}

    return hr;
}

HRESULT InitPixelShaders()
{
    HRESULT hr = S_OK;
    // Refer https://codeshare.io/5PJjxP for g_PS_Y & g_PS_UV blobs
    try
    {
        UINT Size = ARRAYSIZE(g_PS_Y);
        hr = m_pD3D11Device->CreatePixelShader(g_PS_Y, Size, nullptr, &m_pPixelShaderChroma);

        IF_FAILED_THROW(hr);

        Size = ARRAYSIZE(g_PS_UV);
        hr = m_pD3D11Device->CreatePixelShader(g_PS_UV, Size, nullptr, &m_pPixelShaderLuma);

        IF_FAILED_THROW(hr);
    }
    catch (HRESULT) {}

    return hr;
}
Ram
sumber
Ini harus diperiksa, tetapi saya berpikir bahwa pada perangkat keras di mana VideoProcessor hanya dapat keluaran ke YUY2, encoder perangkat keras juga akan menerima YUY2. Jadi, Anda dapat memeriksanya dan mengumpankan output VideoProcessor ke enkoder langsung dalam kasus ini.
VuVirt
@VuVirt, saya juga berpikir yang sama, tetapi ketika saya mencoba untuk menghitung enkoder perangkat keras dengan YUY2 sebagai tipe input saya tidak mendapatkan encoder kembali.
Ram
Apakah mungkin Anda mencoba pada PC GPU ganda?
VuVirt
Saya yakin, saya tidak menjalankannya pada mesin dengan banyak kartu grafis. Saya masih bertanya-tanya bagaimana ketidakcocokan seperti ini bisa terjadi. Saya akan mencoba memperbarui detail lainnya di utas ini.
Ram

Jawaban:

5

Saya mencoba konversi RGBA ini ke NV12 hanya dalam GPU, menggunakan DirectX11.

Ini tantangan yang bagus. Saya tidak terbiasa dengan Directx11, jadi ini adalah eksperimen pertama saya.

Periksa proyek ini untuk pembaruan: D3D11ShaderNV12

Dalam implementasi saya saat ini (mungkin bukan yang terakhir), inilah yang saya lakukan:

  • Langkah 1: gunakan DXGI_FORMAT_B8G8R8A8_UNORM sebagai tekstur input
  • Langkah 2: buat shader pass pertama untuk mendapatkan 3 tekstur (Y: Luma, U: ChromaCb dan V: ChromaCr): lihat YCbCrPS2.hlsl
  • Langkah 3: Y adalah DXGI_FORMAT_R8_UNORM, dan siap untuk tekstur NV12 akhir
  • Langkah 4: UV harus di-downsample dalam 2nd pass shader: lihat ScreenPS2.hlsl (menggunakan penyaringan linier)
  • Langkah 5: shader pass ketiga untuk sampel tekstur Y
  • Langkah 6: lulus shader keempat untuk sampel tekstur UV menggunakan tekstur shift (saya pikir teknik lain bisa digunakan)

ShaderNV12

Tekstur terakhir saya bukan DXGI_FORMAT_NV12, tetapi tekstur DXGI_FORMAT_R8_UNORM yang serupa. Komputer saya adalah Windows7, jadi DXGI_FORMAT_NV12 tidak ditangani. Saya akan coba nanti di komputer lain.

Proses dengan gambar:

RenderTarget

mofo77
sumber
Bagus. Inilah yang saya cari. Terima kasih.
Ram
Anda dapat mencoba mengganti pass render kedua Anda dengan ID3D11DeviceContext :: GenerateMips call. Ini diterapkan jauh di dalam driver GPU, mungkin lebih cepat daripada pass render tambahan dalam kode Anda.
Soonts
Saya tidak tahu apakah ini lebih cepat atau tidak, tetapi saya menambahkan varian untuk menggunakan GenerateMips, bukan shader. Ini adalah teknik yang menarik. Terima kasih atas tipsnya.
mofo77