Can NvTransform be eliminated from JPEG decode pipeline?
Environment
- Platform: Jetson AGX orin
- Camera: MJPEG, 3840x1080, target ≥60fps
- Decoder: NvJPEGDecoder::decodeToFd()
Current Pipeline
MJPEG frame → decodeToFd() → NvTransform() → EGL Map → NPP YUV420ToRGB → D2H
Performance Breakdown (per frame)
┌───────────────────┬───────────┬────────────────────────────────────────────────────────────────────────────────┐
│ Step │ Time (ms) │ Notes │
├───────────────────┼───────────┼────────────────────────────────────────────────────────────────────────────────┤
│ decodeToFd │ 6.2 │ NVDEC hardware decode, outputs to NVBUF_MEM_DEFAULT │
├───────────────────┼───────────┼────────────────────────────────────────────────────────────────────────────────┤
│ NvTransform │ 6.4~9.0 │ Same resolution, same format copy: NVBUF_MEM_DEFAULT → NVBUF_MEM_SURFACE_ARRAY │
├───────────────────┼───────────┼────────────────────────────────────────────────────────────────────────────────┤
│ YUV420ToRGB (NPP) │ 1.6 │ │
├───────────────────┼───────────┼────────────────────────────────────────────────────────────────────────────────┤
│ D2H memcpy │ 1.7 │ │
├───────────────────┼───────────┼────────────────────────────────────────────────────────────────────────────────┤
│ Total │ ~16 │ │
└───────────────────┴───────────┴────────────────────────────────────────────────────────────────────────────────┘
NvTransform accounts for 40%~50% of the total grab time. It performs no format conversion or scaling — it is purely a memory copy from NVBUF_MEM_DEFAULT to NVBUF_MEM_SURFACE_ARRAY so that CUDA can
access the decoded data via EGL mapping. This copy is the single largest bottleneck preventing us from reaching ≥60fps.
Failed Attempts to Remove NvTransform
Attempt 1: Direct EGL mapping on surf_decoder_ (NVBUF_MEM_DEFAULT)
NvBufSurfaceMapEglImage(surf_decoder_, 0);
cuGraphicsEGLRegisterImage(&cudaRes, eglImage, …);
cuGraphicsResourceGetMappedEglFrame(&eglFrame, cudaRes, 0, 0);
// After decodeToFd()
pSrc[0] = (Npp8u *)eglFrame.frame.pPitch[0]; // Y plane — all zeros
Result: CUDA sees all zeros in the mapped memory. NVDEC output is not visible to CUDA via EGL on NVBUF_MEM_DEFAULT buffers, even with NvBufSurfaceSyncForDevice() and
cuGraphicsMapResources/UnmapResources.
Attempt 2: Change surf_decoder_ to NVBUF_MEM_SURFACE_ARRAY
create_params_.memType = NVBUF_MEM_SURFACE_ARRAY; // was NVBUF_MEM_DEFAULT
NvBufSurfaceCreate(&surf_decoder_, 1, &create_params_);
Result: decodeToFd returns success but subsequent access produces:
nvbufsurface: Wrong buffer index (0)
decodeToFd does not support output to NVBUF_MEM_SURFACE_ARRAY buffers.
Questions
- Can decodeToFd output directly to NVBUF_MEM_SURFACE_ARRAY? If so, what configuration is needed?
- Is there any API or mechanism for CUDA to directly access NVDEC output in NVBUF_MEM_DEFAULT without the NvTransform copy?
- Does NvJPEGDecoder provide an alternative decode interface (e.g., decodeToBuffer) that outputs to CUDA-accessible memory?
- Is zero-copy access from CUDA to NVDEC JPEG decode output possible on Jetson? If so, what is the recommended API and workflow?
Goal
Eliminate the NvTransform copy to reduce grab time from ~16ms to ~10ms, enabling ≥60fps for MJPEG cameras.
bool V4l2Cam::decoder_to_fd()
{
int res = ctx_decoder_.jpegdec->decodeToFd(fd_decoder_, ctx_decoder_.in_buffer, ctx_decoder_.in_file_size,
pixfmt_decoder_, dev_width, dev_height);
if (res != 0 || fd_decoder_ < 0) {
LOG(ERROR) << “decodeToFd failed, res=” << res << “, fd_decoder_=” << fd_decoder_;
return false;
}
{
res = NvBufSurf::NvTransform(&transform_params_, fd_decoder_, dst_dma_fds_[0]);
if (res != 0 || dst_dma_fds_[0] < 0) {
LOG(ERROR) << "NvTransform, res=" << res << std::endl;
return false;
}
}
const Npp8u *pSrc[3];
int aSrcStep[3];
pSrc[0] = (Npp8u *)eglFrame_[0].frame.pPitch[0];
aSrcStep[0] = (int)eglFrame_[0].pitch;
pSrc[1] = (Npp8u *)eglFrame_[0].frame.pPitch[1];
pSrc[2] = (Npp8u *)eglFrame_[0].frame.pPitch[2];
aSrcStep[1] = u_pitch_;
aSrcStep[2] = v_pitch_;
NppiSize oSize = {(int)dev_width, (int)dev_height};
Npp8u *pDst = d_rgb_image_;
nppiYUV420ToRGB_8u_P3C3R_Ctx(pSrc, aSrcStep, pDst, dev_width * 3, oSize, npp_rgb_context_);
cudaStreamSynchronize(rgb_stream_);
return true;
}