nvEncRegisterResource()因-23而失败

JPN*_*gon 3 opengl cuda nvidia video-streaming nvenc

在尝试使用NVEnc将OpenGL帧作为H264进行流式传输时,我碰到了一堵完整的砖墙.我已经在这个特殊问题上待了将近8个小时而没有任何进展.

问题是调用nvEncRegisterResource(),代码-23总是失败(枚举值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);
Run Code Online (Sandbox Code Playgroud)

这应该分配设备上的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;
Run Code Online (Sandbox Code Playgroud)

这是砖墙.无论我尝试什么,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);
Run Code Online (Sandbox Code Playgroud)

..然后创建编码会话:

// 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);
Run Code Online (Sandbox Code Playgroud)

最后,初始化编码器的代码:

// 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);
Run Code Online (Sandbox Code Playgroud)

所有上述初始化都报告成功.

我非常感谢任何能让我超越这个障碍的人.


编辑:这是重现问题的完整代码.与原始代码唯一可观察的差异是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\(或类似)在包含路径中.

新编辑:修改代码只使用CUDA驱动程序接口,而不是与运行时API混合.仍然是相同的错误代码.

#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;
}
Run Code Online (Sandbox Code Playgroud)

JPN*_*gon 5

在将NVidia样本NvEncoderCudaInterop与我的最小代码进行比较后,我终于找到了成功与失败之间区别的项目:它传递给pitchNV_ENC_REGISTER_RESOURCE结构参数nvEncRegisterResource().

我没有在任何地方看到它的记录,但是这个值有一个硬限制,我已经通过实验确定它在2560.任何高于此值将导致NV_ENC_ERR_RESOURCE_REGISTER_FAILED.

我传递的音高是通过另一个API调用来计算的cuMemAllocPitch().

(这是从我的代码所缺少的另一件事情是"锁定"和解锁CUDA上下文通过当前线程cuCtxPushCurrent()cuCtxPopCurrent().通过RAII类样品中完成.)


编辑:

我通过做一些事情解决了这个问题,我有另一个原因:使用NV12作为编码器的输入格式而不是YUV444.

对于NV12,pitch参数降至2560限制以下,因为每行的字节大小等于宽度,因此在我的情况下为1920字节.

这是必要的(当时),因为我的显卡是带有"Kepler"GPU的GTX 760,(我最初没有意识到)只支持NV12作为NVEnc的输入格式.我已经升级到GTX 970,但正如我刚刚发现的那样,2560的限制仍然存在.

这让我想知道一个人应该如何使用NVEnc和YUV444.我想到的唯一可能性就是使用节制内存,这看起来很奇怪.我非常感谢那些真正使用过YUV444的NVEnc的人的评论.


编辑#2 - 等待进一步更新:

新信息以另一个SO问题的形式出现:NVencs输出比特流不可读

到目前为止,我的答案很可能是错误的.现在看来,不仅应该在注册CUDA资源时设置音高,而且还应该在实际将音频发送到编码器时进行设置nvEncEncodePicture().我现在无法检查,但下次我会参与该项目.