3

在尝试使用 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);

这应该分配设备上的 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\(或类似的)在包含路径中。

新编辑:修改代码以仅使用 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;
}
4

1 回答 1

5

在将 NVidia 示例NvEncoderCudaInterop与我的最小代码进行比较后,我终于找到了决定成功与失败的项目:它是pitch传递NV_ENC_REGISTER_RESOURCEnvEncRegisterResource().

我还没有在任何地方看到它的记录,但是该值有一个硬性限制,我通过实验确定为 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 一起使用。我想到的唯一可能是使用音调记忆,这似乎很奇怪。我会感谢那些实际使用 NVEnc 和 YUV444 的人的评论。


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

新信息以另一个 SO 问题的形式浮出水面:NVencs Output Bitstream is not readable

到目前为止,我的回答很可能是错误的。现在看来,不仅应该在注册 CUDA 资源时设置音高,而且在通过nvEncEncodePicture(). 我现在无法检查这个,但下次我会在那个项目上工作。

于 2015-10-04T14:54:34.307 回答