3

NVEnc を使用して OpenGL フレームを H264 としてストリーミングしようとして、完全なレンガの壁にぶつかりました。私はこの特定の問題に8時間近く取り組んできましたが、何の進展もありませんでした。

問題は への呼び出しnvEncRegisterResource()です。コード -23 で必ず失敗します (enum 値 NV_ENC_ERR_RESOURCE_REGISTER_FAILED、「リソースの登録に失敗しました」と記載されています - NVidia に感謝します)。

私はオスロ大学のこのドキュメント(54 ページ、「OpenGL 相互運用」) で概説されている手順に従おうとしているので、残念ながらこのドキュメントではコード自体が提供されていませんが、これが機能するはずであるという事実を知っています。 .

アイデアはかなり単純です。

  1. OpenGL フレーム バッファ オブジェクトによって生成されたテクスチャを CUDA にマップします。
  2. テクスチャを (以前に割り当てられた) CUDA バッファにコピーします。
  3. そのバッファを NVEnc 入力リソースとしてマップします
  4. その入力リソースをエンコードのソースとして使用します

私が言ったように、問題はステップ(3)です。関連するコード スニペットを次に示します (簡潔にするために、エラー処理は省略しています)。

// Round up width and height
priv->encWidth = (_resolution.w + 31) & ~31, priv->encHeight = (_resolution.h + 31) & ~31;

// Allocate CUDA "pitched" memory to match the input texture (YUV, one byte per component)
cuErr = cudaMallocPitch(&priv->cudaMemPtr, &priv->cudaMemPitch, 3 * priv->encWidth, priv->encHeight);

これにより、デバイス上の CUDA メモリが割り当てられるはずです (「ピッチ付き」の種類ですが、ピッチなしも試しましたが、結果は変わりません)。

// Register the CUDA buffer as an input resource
NV_ENC_REGISTER_RESOURCE regResParams = { 0 };
regResParams.version = NV_ENC_REGISTER_RESOURCE_VER;
regResParams.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR;
regResParams.width  = priv->encWidth;
regResParams.height = priv->encHeight;
regResParams.bufferFormat = NV_ENC_BUFFER_FORMAT_YUV444_PL;
regResParams.resourceToRegister = priv->cudaMemPtr;
regResParams.pitch = priv->cudaMemPitch;
encStat = nvEncApi.nvEncRegisterResource(priv->nvEncoder, &regResParams);
//                 ^^^ FAILS
priv->nvEncInpRes = regResParams.registeredResource;

これがレンガの壁です。何をしようとしても、nvEncRegisterResource()失敗します。

私は、必要な初期化をすべて行ったと考えています (間違っているかもしれませんが)。CUDA コンテキストを作成してアクティブ化するコードは次のとおりです。

// Pop the current context
cuRes = cuCtxPopCurrent(&priv->cuOldCtx);

// Create a context for the device
priv->cuCtx = nullptr;
cuRes = cuCtxCreate(&priv->cuCtx, CU_CTX_SCHED_BLOCKING_SYNC, priv->cudaDevice);

// Push our context
cuRes = cuCtxPushCurrent(priv->cuCtx);

.. エンコーディング セッションの作成が続きます。

// Create an NV Encoder session
NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS nvEncSessParams = { 0 };
nvEncSessParams.apiVersion = NVENCAPI_VERSION;
nvEncSessParams.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER;
nvEncSessParams.deviceType = NV_ENC_DEVICE_TYPE_CUDA;
nvEncSessParams.device = priv->cuCtx; // nullptr
auto encStat = nvEncApi.nvEncOpenEncodeSessionEx(&nvEncSessParams, &priv->nvEncoder);

最後に、エンコーダーを初期化するコード:

// Configure the encoder via preset 
NV_ENC_PRESET_CONFIG presetConfig = { 0 };
GUID codecGUID = NV_ENC_CODEC_H264_GUID;
GUID presetGUID = NV_ENC_PRESET_LOW_LATENCY_DEFAULT_GUID;
presetConfig.version = NV_ENC_PRESET_CONFIG_VER;
presetConfig.presetCfg.version = NV_ENC_CONFIG_VER;
encStat = nvEncApi.nvEncGetEncodePresetConfig(priv->nvEncoder, codecGUID, presetGUID, &presetConfig);

NV_ENC_INITIALIZE_PARAMS initParams = { 0 };
initParams.version = NV_ENC_INITIALIZE_PARAMS_VER;
initParams.encodeGUID = codecGUID;
initParams.encodeWidth  = priv->encWidth;
initParams.encodeHeight = priv->encHeight;
initParams.darWidth  = 1;
initParams.darHeight = 1;
initParams.frameRateNum = 25;   // TODO: make this configurable
initParams.frameRateDen = 1;    // ditto
//   .max_surface_count = (num_mbs >= 8160) ? 32 : 48;
//   .buffer_delay ? necessary
initParams.enableEncodeAsync = 0;
initParams.enablePTD = 1;
initParams.presetGUID = presetGUID;
memcpy(&priv->nvEncConfig, &presetConfig.presetCfg, sizeof(priv->nvEncConfig));
initParams.encodeConfig = &priv->nvEncConfig;
encStat = nvEncApi.nvEncInitializeEncoder(priv->nvEncoder, &initParams);

上記の初期化はすべて成功を報告します。

このハードルを乗り越えてくれる人には本当に感謝しています。


編集:問題を再現するための完全なコードは次のとおりです。元のコードとの唯一の違いは、cuPopContext()ここでエラー (無視できます) を返すことです。おそらく、私の元のプログラムは、OpenGL を使用することの副作用として、このようなコンテキストを作成します。それ以外の場合、コードは元のコードとまったく同じように動作します。Visual Studio 2013 でコードをビルドしました。次のライブラリ ファイルをリンクする必要があります (C: でない場合はパスを調整します)。C:\Program Files (x86)\NVIDIA GPU Computing Toolkit\CUDA\v7.5\lib\Win32\cuda.lib

C:\Program Files (x86)\NVIDIA GPU Computing Toolkit\CUDA\v7.5\include\また、 (または類似の) がインクルード パスにあることを確認する必要があります。

新しい編集: ランタイム API と混合する代わりに、CUDA ドライバー インターフェイスのみを使用するようにコードを変更しました。それでも同じエラーコードです。

#ifdef _WIN32
#include <Windows.h>
#endif
#include <cassert>
#include <GL/gl.h>
#include <iostream>
#include <string>

#include <stdexcept>
#include <string>

#include <cuda.h>
//#include <cuda_runtime.h>
#include <cuda_gl_interop.h>
#include <nvEncodeAPI.h>

// NV Encoder API ---------------------------------------------------

#if defined(_WIN32)
#define LOAD_FUNC(l, s) GetProcAddress(l, s)
#define DL_CLOSE_FUNC(l) FreeLibrary(l)
#else
#define LOAD_FUNC(l, s) dlsym(l, s)
#define DL_CLOSE_FUNC(l) dlclose(l)
#endif

typedef NVENCSTATUS(NVENCAPI* PNVENCODEAPICREATEINSTANCE)(NV_ENCODE_API_FUNCTION_LIST *functionList);

struct NVEncAPI : public NV_ENCODE_API_FUNCTION_LIST {
public:
    // ~NVEncAPI() { cleanup(); }

    void init() {
#if defined(_WIN32)
        if (sizeof(void*) == 8) {
            nvEncLib = LoadLibrary(TEXT("nvEncodeAPI64.dll"));
        }
        else {
            nvEncLib = LoadLibrary(TEXT("nvEncodeAPI.dll"));
        }
        if (nvEncLib == NULL) throw std::runtime_error("Failed to load NVidia Encoder library: " + std::to_string(GetLastError()));
#else
        nvEncLib = dlopen("libnvidia-encode.so.1", RTLD_LAZY);
        if (nvEncLib == nullptr)
            throw std::runtime_error("Failed to load NVidia Encoder library: " + std::string(dlerror()));
#endif
        auto nvEncodeAPICreateInstance = (PNVENCODEAPICREATEINSTANCE) LOAD_FUNC(nvEncLib, "NvEncodeAPICreateInstance");

        version = NV_ENCODE_API_FUNCTION_LIST_VER;
        NVENCSTATUS encStat = nvEncodeAPICreateInstance(static_cast<NV_ENCODE_API_FUNCTION_LIST *>(this));
    }

    void cleanup() {
#if defined(_WIN32)
        if (nvEncLib != NULL) {
            FreeLibrary(nvEncLib);
            nvEncLib = NULL;
        }
#else
        if (nvEncLib != nullptr) {
            dlclose(nvEncLib);
            nvEncLib = nullptr;
        }
#endif
    }

private:

#if defined(_WIN32)
    HMODULE nvEncLib;
#else
    void* nvEncLib;
#endif
    bool init_done;
};

static NVEncAPI nvEncApi;

// Encoder class ----------------------------------------------------

class Encoder {
public:
    typedef unsigned int uint_t;
    struct Size { uint_t w, h; };

    Encoder() { 
        CUresult cuRes = cuInit(0);
        nvEncApi.init(); 
    }

    void init(const Size & resolution, uint_t texture) {

        NVENCSTATUS encStat;
        CUresult cuRes;

        texSize = resolution;
        yuvTex = texture;

        // Purely for information
        int devCount = 0;
        cuRes = cuDeviceGetCount(&devCount);

        // Initialize NVEnc
        initEncodeSession();            // start an encoding session
        initEncoder();

        // Register the YUV texture as a CUDA graphics resource
        // CODE COMMENTED OUT AS THE INPUT TEXTURE IS NOT NEEDED YET (TO MY UNDERSTANDING) AT SETUP TIME
        //cudaGraphicsGLRegisterImage(&priv->cudaInpTexRes, priv->yuvTex, GL_TEXTURE_2D, cudaGraphicsRegisterFlagsReadOnly);

        // Allocate CUDA "pitched" memory to match the input texture (YUV, one byte per component)
        encWidth = (texSize.w + 31) & ~31, encHeight = (texSize.h + 31) & ~31;
        cuRes = cuMemAllocPitch(&cuDevPtr, &cuMemPitch, 4 * encWidth, encHeight, 16);

        // Register the CUDA buffer as an input resource
        NV_ENC_REGISTER_RESOURCE regResParams = { 0 };
        regResParams.version = NV_ENC_REGISTER_RESOURCE_VER;
        regResParams.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR;
        regResParams.width = encWidth;
        regResParams.height = encHeight;
        regResParams.bufferFormat = NV_ENC_BUFFER_FORMAT_YUV444_PL;
        regResParams.resourceToRegister = (void*) cuDevPtr;
        regResParams.pitch = cuMemPitch;
        encStat = nvEncApi.nvEncRegisterResource(nvEncoder, &regResParams);
        assert(encStat == NV_ENC_SUCCESS); // THIS IS THE POINT OF FAILURE
        nvEncInpRes = regResParams.registeredResource;
    }

    void cleanup() { /* OMITTED */ }

    void encode() {
        // THE FOLLOWING CODE WAS NEVER REACHED YET BECAUSE OF THE ISSUE.
        // INCLUDED HERE FOR REFERENCE.

        CUresult cuRes;
        NVENCSTATUS encStat;

        cuRes = cuGraphicsResourceSetMapFlags(cuInpTexRes, CU_GRAPHICS_MAP_RESOURCE_FLAGS_READ_ONLY);

        cuRes = cuGraphicsMapResources(1, &cuInpTexRes, 0);

        CUarray mappedArray;
        cuRes = cuGraphicsSubResourceGetMappedArray(&mappedArray, cuInpTexRes, 0, 0);

        cuRes = cuMemcpyDtoA(mappedArray, 0, cuDevPtr, 4 * encWidth * encHeight);

        NV_ENC_MAP_INPUT_RESOURCE mapInputResParams = { 0 };
        mapInputResParams.version = NV_ENC_MAP_INPUT_RESOURCE_VER;
        mapInputResParams.registeredResource = nvEncInpRes;
        encStat = nvEncApi.nvEncMapInputResource(nvEncoder, &mapInputResParams);

        // TODO: encode...

        cuRes = cuGraphicsUnmapResources(1, &cuInpTexRes, 0);
    }

private:
    struct PrivateData;

    void initEncodeSession() {

        CUresult cuRes;
        NVENCSTATUS encStat;

        // Pop the current context
        cuRes = cuCtxPopCurrent(&cuOldCtx); // THIS IS ALLOWED TO FAIL (it doesn't

        // Create a context for the device
        cuCtx = nullptr;
        cuRes = cuCtxCreate(&cuCtx, CU_CTX_SCHED_BLOCKING_SYNC, 0);

        // Push our context
        cuRes = cuCtxPushCurrent(cuCtx);

        // Create an NV Encoder session
        NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS nvEncSessParams = { 0 };
        nvEncSessParams.apiVersion = NVENCAPI_VERSION;
        nvEncSessParams.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER;
        nvEncSessParams.deviceType = NV_ENC_DEVICE_TYPE_CUDA;
        nvEncSessParams.device = cuCtx;
        encStat = nvEncApi.nvEncOpenEncodeSessionEx(&nvEncSessParams, &nvEncoder);
    }

    void Encoder::initEncoder()
    {
        NVENCSTATUS encStat;

        // Configure the encoder via preset 
        NV_ENC_PRESET_CONFIG presetConfig = { 0 };
        GUID codecGUID = NV_ENC_CODEC_H264_GUID;
        GUID presetGUID = NV_ENC_PRESET_LOW_LATENCY_DEFAULT_GUID;
        presetConfig.version = NV_ENC_PRESET_CONFIG_VER;
        presetConfig.presetCfg.version = NV_ENC_CONFIG_VER;
        encStat = nvEncApi.nvEncGetEncodePresetConfig(nvEncoder, codecGUID, presetGUID, &presetConfig);

        NV_ENC_INITIALIZE_PARAMS initParams = { 0 };
        initParams.version = NV_ENC_INITIALIZE_PARAMS_VER;
        initParams.encodeGUID = codecGUID;
        initParams.encodeWidth = texSize.w;
        initParams.encodeHeight = texSize.h;
        initParams.darWidth = texSize.w;
        initParams.darHeight = texSize.h;
        initParams.frameRateNum = 25;
        initParams.frameRateDen = 1;
        initParams.enableEncodeAsync = 0;
        initParams.enablePTD = 1;
        initParams.presetGUID = presetGUID;
        memcpy(&nvEncConfig, &presetConfig.presetCfg, sizeof(nvEncConfig));
        initParams.encodeConfig = &nvEncConfig;
        encStat = nvEncApi.nvEncInitializeEncoder(nvEncoder, &initParams);
    }

    //void cleanupEncodeSession();
    //void cleanupEncoder;

    Size                    texSize;

    GLuint                  yuvTex;
    uint_t                  encWidth, encHeight;
    CUdeviceptr             cuDevPtr;
    size_t                  cuMemPitch;
    NV_ENC_CONFIG           nvEncConfig;
    NV_ENC_INPUT_PTR        nvEncInpBuf;
    NV_ENC_REGISTERED_PTR   nvEncInpRes;
    CUdevice                cuDevice;
    CUcontext               cuCtx, cuOldCtx;
    void                    *nvEncoder;
    CUgraphicsResource      cuInpTexRes;
};


int main(int argc, char *argv[])
{
    Encoder encoder;

    encoder.init({1920, 1080}, 0); // OMITTED THE TEXTURE AS IT IS NOT NEEDED TO REPRODUCE THE ISSUE

    return 0;
}
4

1 に答える 1

5

NVidia のサンプルNvEncoderCudaInteropを私の最小限のコードと比較した後、最終的に成功と失敗の違いを生む項目を見つけました。それは、に渡される構造のpitchパラメーターです。NV_ENC_REGISTER_RESOURCEnvEncRegisterResource()

どこにも文書化されているのを見たことがありませんが、その値には厳しい制限があり、実験的に 2560 であると判断しました。それを超えると、NV_ENC_ERR_RESOURCE_REGISTER_FAILED になります。

私が渡していたピッチが別の API 呼び出しによって計算されたものであることは問題ではないようcuMemAllocPitch()です。

(私のコードに欠けていたもう 1 つのことは、CUDA コンテキストをcuCtxPushCurrent()およびを介して現在のスレッドに「ロック」およびロック解除することcuCtxPopCurrent()でした。RAII クラスを介してサンプルで行われました。)


編集:

YUV444 の代わりにエンコーダの入力形式として NV12 を使用するという別の理由があったため、この問題を回避しました。

NV12では、pitch行あたりのバイトサイズが幅と等しいため、パラメーターは2560の制限を下回り、私の場合は1920バイトです。

私のグラフィックス カードは「Kepler」GPU を搭載した GTX 760 であり、(最初は気づいていなかったので) NVEnc の入力形式として NV12 しかサポートしていないため、これは (当時) 必要でした。その後、GTX 970 にアップグレードしましたが、2560 の制限がまだ残っていることがわかりました。

これは、YUV444 で NVEnc をどの程度正確に使用することが期待されているのか疑問に思います。私の頭に浮かぶ唯一の可能性は、ピッチのないメモリを使用することです。これは奇妙に思えます。実際に YUV444 で NVEnc を使ったことのある方のコメントをいただければ幸いです。


編集 #2 - 保留中のさらなる更新:

新しい情報は、別の SO の質問の形で浮上しています: NVencs 出力ビットストリームは読み取り不能です

これまでの私の答えが間違っていた可能性は十分にあります。ピッチは、CUDA リソースを登録するときだけでなく、実際に 経由でエンコーダーに送信するときにも設定する必要があるようnvEncEncodePicture()です。今は確認できませんが、次回そのプロジェクトに取り組むときに確認します。

于 2015-10-04T14:54:34.307 に答える