`NvBufSurface`是NVIDIA Jetson Multimedia API、DeepStream、GStreamer NVMM插件以及众多相机和视频流水线所使用的通用图像缓冲区容器。一个常见的错误来源是假定每个`NvBufSurfaceParams::dataPtr`都是CUDA设备指针。
事实并非如此。
将surface暴露给CUDA的正确方式取决于两个属性:
-
`NvBufSurface::memType`------内存由谁分配和拥有。
-
`NvBufSurfaceParams::layout`------图像采用pitch-linear(间距线性)布局还是block-linear(块线性)布局。
本文介绍以下Jetson内存类型:
-
`NVBUF_MEM_CUDA_PINNED`
-
`NVBUF_MEM_CUDA_DEVICE`
-
`NVBUF_MEM_CUDA_UNIFIED`
-
`NVBUF_MEM_SURFACE_ARRAY`
-
`NVBUF_MEM_CUDA_ARRAY`
示例使用CUDA Runtime API以及近期JetPack版本中提供的`nvbufsurface.h` API。某些较新的API,尤其是`NvBufSurfaceMapCudaBuffer()`,取决于具体版本和平台,因此本文也提供了EGLImage回退方案。
1. 首先检查surface
一个`NvBufSurface`表示一个批次。每次针对`surfaceList`中的一个条目获取CUDA访问:
```cpp
#include <cuda_runtime_api.h>
#include <nvbufsurface.h>
#include <cstdint>
#include <stdexcept>
#include <string>
void checkCuda(cudaError_t status, char const* operation)
{
if (status != cudaSuccess)
{
throw std::runtime_error(
std::string(operation) + ": " + cudaGetErrorString(status));
}
}
void checkNvBuf(int status, char const* operation)
{
if (status != 0)
{
throw std::runtime_error(std::string(operation) + " failed");
}
}
void inspectSurface(NvBufSurface const* surface, unsigned int batchIndex)
{
if (surface == nullptr || surface->surfaceList == nullptr)
throw std::runtime_error("invalid NvBufSurface");
if (batchIndex >= surface->batchSize)
throw std::runtime_error("batch index is out of range");
NvBufSurfaceParams const& image = surface->surfaceListbatchIndex;
NvBufSurfacePlaneParams const& planes = image.planeParams;
// Always inspect these before selecting an access path.
NvBufSurfaceMemType memoryType = surface->memType;
NvBufSurfaceLayout layout = image.layout;
unsigned int planeCount = planes.num_planes;
(void) memoryType;
(void) layout;
(void) planeCount;
}
```
对于多平面格式,不要根据宽度和高度计算平面地址。应使用`planeParams`提供的元数据:
```cpp
planes.offsetp // byte offset from the allocation base
planes.pitchp // bytes between neighboring rows
planes.widthp // logical plane width
planes.heightp // logical plane height
planes.bytesPerPixp // bytes per plane element
```
例如,NV12通常包含一个全分辨率Y平面和一个半分辨率的交错UV平面。两个平面的行都可能包含填充,因此`pitchp`不一定等于`widthp * bytesPerPixp`。
2. 各内存类型的访问规则
`NVBUF_MEM_CUDA_DEVICE`
这是CUDA设备内存分配。`surfaceListi.dataPtr`可由CUDA kernel和CUDA复制API直接使用,但不能通过普通的CPU加载和存储操作访问。
对于pitch-linear图像,从基址指针派生各个平面:
```cpp
NvBufSurfaceParams& image = surface->surfaceListbatchIndex;
NvBufSurfacePlaneParams const& planes = image.planeParams;
auto* base = static_cast<std::uint8_t*>(image.dataPtr);
if (base == nullptr)
throw std::runtime_error("CUDA device surface has no dataPtr");
void* yDevice = base + planes.offset0;
std::size_t yPitch = planes.pitch0;
void* uvDevice = base + planes.offset1;
std::size_t uvPitch = planes.pitch1;
```
将`yDevice`和`uvDevice`作为设备指针传给kernel。kernel必须使用所提供的pitch:
```cpp
global void invertLuma(
std::uint8_t* y, std::size_t pitch, unsigned int width, unsigned int height)
{
unsigned int x = blockIdx.x * blockDim.x + threadIdx.x;
unsigned int row = blockIdx.y * blockDim.y + threadIdx.y;
if (x < width && row < height)
yrow \* pitch + x = 255 - yrow \* pitch + x;
}
```
无需调用`NvBufSurfaceMap()`即可获取此CUDA指针。
`NVBUF_MEM_CUDA_UNIFIED`
这是CUDA托管内存。`dataPtr`是托管指针,CPU和CUDA kernel均可使用:
```cpp
auto* base =
static_cast<std::uint8_t*>(surface->surfaceListbatchIndex.dataPtr);
```
各平面的地址同样为`base + planeParams.offsetp`。
托管内存很方便,但方便并不等同于延迟可预测。CPU和GPU访问可能会引发迁移或一致性维护。在采用集成式Jetson GPU的系统上,不存在独立GPU的PCIe传输,但同步和页面管理开销仍然存在。在延迟敏感的帧循环中,应避免交替切换CPU和GPU所有权,除非性能分析表明这种做法可以接受。
在一个处理器使用另一个处理器写入的数据之前,仍需要使用CUDA event、stream同步或应用程序级所有权管理。
`NVBUF_MEM_CUDA_PINNED`
这是页锁定主机内存。`dataPtr`是已向CUDA注册的主机指针。其主要用途是进行快速异步传输:
```cpp
NvBufSurfaceParams& image = surface->surfaceListbatchIndex;
auto* hostBase = static_cast<std::uint8_t*>(image.dataPtr);
checkCuda(
cudaMemcpy2DAsync(
destinationDevice,
destinationPitch,
hostBase + image.planeParams.offset0,
image.planeParams.pitch0,
image.planeParams.width0 * image.planeParams.bytesPerPix0,
image.planeParams.height0,
cudaMemcpyHostToDevice,
stream),
"cudaMemcpy2DAsync");
```
不要直接将pinned主机指针当作来自`cudaMalloc()`的指针传给kernel。如果确实需要零拷贝的映射主机访问,应查询设备别名并检查该内存分配是否支持它:
```cpp
void* deviceAlias = nullptr;
checkCuda(
cudaHostGetDevicePointer(&deviceAlias, image.dataPtr, 0),
"cudaHostGetDevicePointer");
```
在使用统一虚拟寻址的系统上,主机地址和设备地址的数值可能相同。代码不应依赖这一点。此外还应对这条路径进行性能分析:映射主机内存适用于某些访问模式,但kernel反复读取它通常比先将图像暂存到设备内存中更慢。
`NVBUF_MEM_CUDA_ARRAY`
这种内存由CUDA array表示,而不是线性指针。在当前Jetson Multimedia API版本中,`surfaceListi.dataPtr`携带一个`cudaArray_t`句柄:
```cpp
NvBufSurfaceParams& image = surface->surfaceListbatchIndex;
auto imageArray = static_cast<cudaArray_t>(image.dataPtr);
if (imageArray == nullptr)
throw std::runtime_error("CUDA array surface has no array handle");
```
对于多平面图像,获取各个平面:
```cpp
cudaArray_t yArray = nullptr;
cudaArray_t uvArray = nullptr;
checkCuda(cudaArrayGetPlane(&yArray, imageArray, 0),
"cudaArrayGetPlane(Y)");
checkCuda(cudaArrayGetPlane(&uvArray, imageArray, 1),
"cudaArrayGetPlane(UV)");
```
CUDA array不能像`std::uint8_t*`一样被解引用。应使用以下接口之一:
-
用于读取访问的CUDA texture object
-
用于读写访问的CUDA surface object
-
`cudaMemcpy2DFromArrayAsync()`或`cudaMemcpy2DToArrayAsync()`
例如,为一个平面创建texture object:
```cpp
cudaResourceDesc resource{};
resource.resType = cudaResourceTypeArray;
resource.res.array.array = yArray;
cudaTextureDesc texture{};
texture.addressMode0 = cudaAddressModeClamp;
texture.addressMode1 = cudaAddressModeClamp;
texture.filterMode = cudaFilterModePoint;
texture.readMode = cudaReadModeElementType;
texture.normalizedCoords = 0;
cudaTextureObject_t yTexture = 0;
checkCuda(
cudaCreateTextureObject(&yTexture, &resource, &texture, nullptr),
"cudaCreateTextureObject");
// Launch kernels that read yTexture...
checkCuda(cudaDestroyTextureObject(yTexture),
"cudaDestroyTextureObject");
```
`NVBUF_MEM_SURFACE_ARRAY`
Surface-array内存是Jetson原生的NVMM/NVRM图像表示形式。它通常由相机采集、硬件视频解码、VIC和DeepStream元素生成。它可以导出为DMA-BUF,并且可能采用block-linear布局。
以下两个字段经常被误解:
-
对于这种内存类型,`surfaceListi.dataPtr`不是CUDA设备指针。
-
`NvBufSurfaceMap()`会在`surfaceListi.mappedAddr.addrp`中创建CPU映射;该指针同样不是CUDA设备指针。
请使用下面两种CUDA互操作路径之一。
3. 近期JetPack上的surface-array访问:CUDA缓冲区映射
近期Jetson版本提供了`NvBufSurfaceMapCudaBuffer()`:
```cpp
unsigned int i = batchIndex;
checkNvBuf(
NvBufSurfaceMapCudaBuffer(surface, static_cast<int>(i)),
"NvBufSurfaceMapCudaBuffer");
auto* cudaBuffer = static_cast<NvBufSurfaceCudaBuffer*>(
surface->surfaceListi.mappedAddr.cudaPtr);
if (cudaBuffer == nullptr)
{
NvBufSurfaceUnMapCudaBuffer(surface, static_cast<int>(i));
throw std::runtime_error("CUDA mapping returned no NvBufSurfaceCudaBuffer");
}
```
Pitch-linear surface array
对于pitch-linear surface,近期Jetson实现会通过`cudaBuffer->dataPtr`暴露CUDA可访问的线性内存分配。应使用surface的平面偏移和pitch:
```cpp
NvBufSurfaceParams& image = surface->surfaceListi;
NvBufSurfacePlaneParams const& planes = image.planeParams;
if (image.layout != NVBUF_LAYOUT_PITCH)
throw std::runtime_error("a linear pointer requires pitch-linear layout");
auto* base = static_cast<std::uint8_t*>(cudaBuffer->dataPtr);
if (base == nullptr)
throw std::runtime_error("pitch-linear mapping returned no data pointer");
void* yDevice = base + planes.offset0;
void* uvDevice = base + planes.offset1;
std::size_t yPitch = planes.pitch0;
std::size_t uvPitch = planes.pitch1;
```
在所有使用这些地址的CUDA工作完成之前,应保持该映射和原始`NvBufSurface`存活。
Block-linear surface array
block-linear图像没有普通的行优先指针。在近期基于OpenRM的Jetson版本上,CUDA映射可以暴露mipmapped array:
```cpp
if (image.layout != NVBUF_LAYOUT_BLOCK_LINEAR)
throw std::runtime_error("expected block-linear layout");
auto mipmap = static_cast<cudaMipmappedArray_t>(cudaBuffer->mipmap);
if (mipmap == nullptr)
throw std::runtime_error("block-linear mapping returned no mipmapped array");
cudaArray_t level0 = nullptr;
checkCuda(
cudaGetMipmappedArrayLevel(&level0, mipmap, 0),
"cudaGetMipmappedArrayLevel");
cudaArray_t yArray = nullptr;
cudaArray_t uvArray = nullptr;
checkCuda(cudaArrayGetPlane(&yArray, level0, 0),
"cudaArrayGetPlane(Y)");
checkCuda(cudaArrayGetPlane(&uvArray, level0, 1),
"cudaArrayGetPlane(UV)");
```
将平面array与CUDA texture、CUDA surface或array复制API配合使用。对于block-linear内存,不要将`cudaBuffer->dataPtr`重新解释为kernel指针。
CUDA工作完成后:
```cpp
checkCuda(cudaStreamSynchronize(stream), "cudaStreamSynchronize");
checkNvBuf(
NvBufSurfaceUnMapCudaBuffer(surface, static_cast<int>(i)),
"NvBufSurfaceUnMapCudaBuffer");
```
这里显式展示stream同步,以明确生命周期规则。在实际流水线中,可以使用event或其他所有权机制来避免同步整个stream。
4. 可移植的surface-array回退方案:EGLImage互操作
如果已安装的SDK不提供`NvBufSurfaceMapCudaBuffer()`,请使用较旧但部署广泛的EGLImage路径:
-
将surface array转换为`EGLImageKHR`。
-
向CUDA注册该图像。
-
获取`cudaEglFrame`。
-
检查CUDA暴露的是pitch还是array。
```cpp
#include <cudaEGL.h>
#include <cuda_runtime_api.h>
#include <EGL/egl.h>
unsigned int i = batchIndex;
checkNvBuf(
NvBufSurfaceMapEglImage(surface, static_cast<int>(i)),
"NvBufSurfaceMapEglImage");
EGLImageKHR eglImage = static_cast<EGLImageKHR>(
surface->surfaceListi.mappedAddr.eglImage);
if (eglImage == EGL_NO_IMAGE_KHR)
{
NvBufSurfaceUnMapEglImage(surface, static_cast<int>(i));
throw std::runtime_error("EGL mapping returned no image");
}
cudaGraphicsResource_t resource = nullptr;
checkCuda(
cudaGraphicsEGLRegisterImage(
&resource, eglImage, cudaGraphicsRegisterFlagsNone),
"cudaGraphicsEGLRegisterImage");
cudaEglFrame frame{};
checkCuda(
cudaGraphicsResourceGetMappedEglFrame(&frame, resource, 0, 0),
"cudaGraphicsResourceGetMappedEglFrame");
```
始终检查`frame.frameType`。
对于pitch表示形式:
```cpp
if (frame.frameType == cudaEglFrameTypePitch)
{
void* plane0 = frame.frame.pPitch0;
void* plane1 = frame.frame.pPitch1;
std::size_t pitch = frame.pitch;
// Use plane0/plane1 as CUDA-accessible pointers.
}
```
对于array表示形式:
```cpp
if (frame.frameType == cudaEglFrameTypeArray)
{
cudaArray_t plane0 = frame.frame.pArray0;
cudaArray_t plane1 = frame.frame.pArray1;
// Use texture, surface, or CUDA array-copy APIs.
}
```
CUDA停止使用该frame后,必须按相反顺序清理:
```cpp
checkCuda(cudaStreamSynchronize(stream), "cudaStreamSynchronize");
checkCuda(
cudaGraphicsUnregisterResource(resource),
"cudaGraphicsUnregisterResource");
checkNvBuf(
NvBufSurfaceUnMapEglImage(surface, static_cast<int>(i)),
"NvBufSurfaceUnMapEglImage");
```
对于通过`cudaGraphicsEGLRegisterImage()`注册的EGLImage,无需调用`cudaGraphicsMapResources()`。
5. 一个实用的分派函数
以下代码框架展示了决策树。生产代码应使用RAII封装每个映射,确保异常和提前返回不会导致资源泄漏。
```cpp
void processWithCuda(
NvBufSurface* surface, unsigned int batchIndex, cudaStream_t stream)
{
NvBufSurfaceParams& image = surface->surfaceListbatchIndex;
switch (surface->memType)
{
case NVBUF_MEM_CUDA_DEVICE:
// Linear CUDA pointer:
// image.dataPtr + image.planeParams.offsetp
processLinearCudaImage(image, stream);
break;
case NVBUF_MEM_CUDA_UNIFIED:
// Managed pointer; synchronize CPU/GPU ownership correctly.
processLinearCudaImage(image, stream);
break;
case NVBUF_MEM_CUDA_PINNED:
// Usually stage with cudaMemcpy2DAsync().
// Use cudaHostGetDevicePointer() only for intentional mapped-host access.
stagePinnedImageAndProcess(image, stream);
break;
case NVBUF_MEM_CUDA_ARRAY:
// image.dataPtr is a cudaArray_t; split multi-planar images with
// cudaArrayGetPlane().
processCudaArrayImage(image, stream);
break;
case NVBUF_MEM_SURFACE_ARRAY:
// Preferred on supported recent releases:
// NvBufSurfaceMapCudaBuffer().
// Fallback: NvBufSurfaceMapEglImage() + CUDA EGL interop.
processSurfaceArrayImage(surface, batchIndex, stream);
break;
default:
throw std::runtime_error("unsupported NvBufSurface memory type");
}
}
```
在Jetson上,`NVBUF_MEM_DEFAULT`通常解析为`NVBUF_MEM_SURFACE_ARRAY`(在thor之前),但应根据分配器返回的实际`surface->memType`进行分派,而不是假定"默认"意味着什么。
6. 同步与所有权
获取地址并不意味着生产者已经完成写入,也不意味着消费者已经完成读取。
Surface-array内存的CPU映射
`NvBufSurfaceMap()`将支持的内存映射到CPU地址空间:
```cpp
checkNvBuf(
NvBufSurfaceMap(surface, batchIndex, -1, NVBUF_MAP_READ_WRITE),
"NvBufSurfaceMap");
```
硬件或设备完成写入后,CPU读取之前:
```cpp
checkNvBuf(
NvBufSurfaceSyncForCpu(surface, batchIndex, -1),
"NvBufSurfaceSyncForCpu");
```
CPU完成写入后,设备读取之前:
```cpp
checkNvBuf(
NvBufSurfaceSyncForDevice(surface, batchIndex, -1),
"NvBufSurfaceSyncForDevice");
```
最后:
```cpp
checkNvBuf(
NvBufSurfaceUnMap(surface, batchIndex, -1),
"NvBufSurfaceUnMap");
```
这些CPU缓存API不能取代CUDA stream同步。
GStreamer和DeepStream缓冲区
当`NvBufSurface`来自`GstBuffer`时,在CUDA使用该surface期间,应保持`GstBuffer`处于已映射和被引用状态。除非保留该缓冲区并安排下游同步,否则pad probe不能启动异步CUDA工作后立即返回,然后继续安全地使用该指针。
一个安全的同步执行顺序是:
-
映射`GstBuffer`。
-
获取`NvBufSurface`。
-
获取CUDA指针或array。
-
启动CUDA工作。
-
等待该工作完成,或使用显式event/fence转移所有权。
-
释放CUDA互操作资源。
-
取消映射并释放`GstBuffer`。
7. 常见错误
错误:将每个`dataPtr`都视为设备指针
`dataPtr`在不同情况下含义不同:
-
CUDA设备内存:CUDA设备指针
-
CUDA统一内存:托管指针
-
CUDA pinned内存:页锁定主机指针
-
CUDA array内存:`cudaArray_t`句柄
-
surface-array内存:不是有效的直接CUDA指针
可使用`cudaPointerGetAttributes()`诊断基于指针的内存,但不要对`cudaArray_t`调用它。
错误:在CUDA kernel中使用`mappedAddr.addrp`
`mappedAddr.addrp`由`NvBufSurfaceMap()`创建,供CPU访问。它不是`NVBUF_MEM_SURFACE_ARRAY`的CUDA映射。
错误:忽略block-linear布局
为提高硬件效率,block-linear内存采用分块布局。它不能通过`row * pitch + column`寻址。应使用CUDA array/texture/surface,或将图像转换/复制到pitch-linear CUDA目标内存中。
错误:假定所有平面共用一个pitch
每个平面都应使用`planeParams.pitchp`。这对于多平面YUV格式尤其重要。
错误:在异步工作完成前释放映射
CUDA启动是异步的。在消费该数据的stream执行完成之前,应保持原始surface、其所有者以及所有互操作映射存活。
错误:混淆内存访问与颜色转换
将NV12映射到CUDA并不会将其转换为RGB。这只会暴露Y和UV存储。色彩范围和转换矩阵同样重要:BT.601、BT.709、BT.2020和扩展范围格式需要不同的转换参数。
8. 选择内存类型
当帧需要在Jetson相机、解码器、编码器、VIC、合成器或其他NVMM硬件模块之间高效流转时,请使用`NVBUF_MEM_SURFACE_ARRAY`。仅在CUDA处理阶段将其映射到CUDA。
当CUDA或TensorRT负责处理流水线中的大部分工作,并且线性设备指针是自然接口时,请使用`NVBUF_MEM_CUDA_DEVICE`。
当texture/surface访问或硬件互操作比线性指针更重要时,请使用`NVBUF_MEM_CUDA_ARRAY`。
将`NVBUF_MEM_CUDA_PINNED`用作CPU可见的暂存内存,以进行异步复制。应将映射主机内存的kernel访问视为需要进行基准测试的优化,而不是默认方案。
当编程便利性很重要,并且已针对目标工作负载分析过其运行时行为时,请使用`NVBUF_MEM_CUDA_UNIFIED`。
不存在普遍适用的最佳类型。最佳选择应最大限度地减少整个流水线中的转换和所有权切换。
9. 最终决策树
拿到一个未知的Jetson `NvBufSurface`时,请按以下顺序处理:
-
读取`surface->memType`。
-
为该内存类型选择对应的访问机制。
-
读取`surfaceListi.layout`。
-
对于pitch-linear存储,使用基址、平面偏移以及每个平面各自的pitch。
-
对于block-linear或CUDA-array存储,通过texture、surface或array复制API使用CUDA array。
-
建立生产者/消费者同步。
-
在异步CUDA工作完成之前,保持surface和映射存活。
-
按相反顺序取消映射或注销资源。
核心规则很简单:
> `NvBufSurface`描述的是内存;它并不保证
> `dataPtr`是CUDA设备指针。
显式处理内存类型、布局、平面几何信息、所有权和同步后,Jetson上的零拷贝CUDA处理将变得可预测。
参考资料
-
NVIDIA DeepStream \`NvBufSurface\` API(https://docs.nvidia.com/metropolis/deepstream/dev-guide/sdk-api/group__ds__aaa.html)
-
CUDA Runtime API:EGL互操作(https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__EGL.html)
-
CUDA Runtime API:内存管理(https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__MEMORY.html)
-
随JetPack安装的Jetson Multimedia API头文件:
`/usr/src/jetson_multimedia_api/include/nvbufsurface.h`