在NVIDIA Jetson上从`NvBufSurface`获取CUDA访问

`NvBufSurface`是NVIDIA Jetson Multimedia API、DeepStream、GStreamer NVMM插件以及众多相机和视频流水线所使用的通用图像缓冲区容器。一个常见的错误来源是假定每个`NvBufSurfaceParams::dataPtr`都是CUDA设备指针。

事实并非如此。

将surface暴露给CUDA的正确方式取决于两个属性:

  1. `NvBufSurface::memType`------内存由谁分配和拥有。

  2. `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路径:

  1. 将surface array转换为`EGLImageKHR`。

  2. 向CUDA注册该图像。

  3. 获取`cudaEglFrame`。

  4. 检查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工作后立即返回,然后继续安全地使用该指针。

一个安全的同步执行顺序是:

  1. 映射`GstBuffer`。

  2. 获取`NvBufSurface`。

  3. 获取CUDA指针或array。

  4. 启动CUDA工作。

  5. 等待该工作完成,或使用显式event/fence转移所有权。

  6. 释放CUDA互操作资源。

  7. 取消映射并释放`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`时,请按以下顺序处理:

  1. 读取`surface->memType`。

  2. 为该内存类型选择对应的访问机制。

  3. 读取`surfaceListi.layout`。

  4. 对于pitch-linear存储,使用基址、平面偏移以及每个平面各自的pitch。

  5. 对于block-linear或CUDA-array存储,通过texture、surface或array复制API使用CUDA array。

  6. 建立生产者/消费者同步。

  7. 在异步CUDA工作完成之前,保持surface和映射存活。

  8. 按相反顺序取消映射或注销资源。

核心规则很简单:

> `NvBufSurface`描述的是内存;它并不保证

> `dataPtr`是CUDA设备指针。

显式处理内存类型、布局、平面几何信息、所有权和同步后,Jetson上的零拷贝CUDA处理将变得可预测。


参考资料

`/usr/src/jetson_multimedia_api/include/nvbufsurface.h`

相关推荐
IT古董13 分钟前
AI 资讯日报 | 2026年8月27日:智谱、阿里接连发布并开源高性能大模型,英伟达交出营收翻倍的超预期财报,工信部明确“十五五“AI发展路线图
人工智能·开源
牧羊人.33314 分钟前
动手学深度学习 01:核心组件与完整训练流程
开发语言·人工智能·深度学习
盟接之桥14 分钟前
半导体供应链破局:EDI如何成为中国制造的数字通行证
大数据·运维·服务器·网络·数据库·人工智能·制造
晓晓_za89866815 分钟前
GEO 搜索源码白帽合规改造:适配各大 AI 信源收录规则
java·开发语言·人工智能·性能优化·开源
集芯微电科技有限公司16 分钟前
低压功率MOSFETs选型手册
人工智能·单片机·嵌入式硬件·神经网络·生成对抗网络
爱分享的康康17 分钟前
从“识别已知”到“发现未知”:3D通用目标检测如何打开智驾感知新边界
人工智能·目标检测·3d
俊哥V21 分钟前
每日 AI 研究简报 · 2026-08-28
人工智能·ai
点PY22 分钟前
《一种超分辨率重建方法和相关装置》专利解析
人工智能·计算机视觉·超分辨率重建
Luke Ewin24 分钟前
Qwen3-ASR藏语语音识别 | 少数民族(藏语|维吾尔语)语音识别 | 本地化部署藏语语音识别模型
人工智能·语音识别·asr·藏语asr