From eae902afb38c0f9099ece01709184ae53ae94252 Mon Sep 17 00:00:00 2001 From: Evgeny Demchenko Date: Wed, 3 Jun 2026 04:42:41 +0100 Subject: [PATCH] =?UTF-8?q?Phase=201=20smoke=20test=20=D0=B7=D0=B5=D0=BB?= =?UTF-8?q?=D1=91=D0=BD=D1=8B=D0=B9:=20=D1=84=D0=B8=D0=BA=D1=81=20frame=5F?= =?UTF-8?q?busy=20+=20staging=20buffer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit После live-теста на R9-88.23 с реальным publisher'ом cam-parking (Dahua 1920x1080 @ 25fps) выявлены и поправлены два блокера: 1) source.c: release_current_frame ПЕРЕД cuframes_subscriber_next. cuframes валидирует frame_busy в начале next (consumer.c:334) и возвращает CUFRAMES_ERR_INVALID_ARG если предыдущий frame не release'нут. Прошлая реализация делала release ПОСЛЕ next — получалось много invalid_arg и каждые ~1с поток re-subscribe'ился, throughput падал до 0.87 fps. После фикса — стабильные 25.1 fps. 2) nvenc.c: добавлен staging buffer cuMemAlloc + cuMemcpy2D в encode_frame. NVENC nvEncRegisterResource возвращает RESOURCE_REGISTER_FAILED для VMM-mapped указателей cuframes v0.4 (CUdeviceptr из cuMemMap). Это известное ограничение NVENC SDK для VMM-памяти. Pre-copy в собственный cuMemAlloc-буфер решает проблему и сохраняет всё в VRAM (cudaMemcpyDeviceToDevice, без CPU bounce). Phase 1.1 будет research как получить настоящий zero-copy (cuMemExportToShareableHandle?). Smoke test результаты (cuframes-dev image + NVIDIA_DRIVER_CAPABILITIES video, /run/cuframes mount + cuframes-ipc-anchor IPC namespace): 300 кадров, 12 IDR, 6.9 МБ за 11.9с (25.1 fps) достигнут лимит 15с flush encoder ffprobe: 377 кадров, 1920x1080 yuv420p H.264 High@4.0 ffmpeg decode → PNG/JPG → реальное изображение с камеры --- src/nvenc.c | 90 +++++++++++++++++++++++++++++++++++++++++++++++++--- src/source.c | 15 ++++++--- 2 files changed, 96 insertions(+), 9 deletions(-) diff --git a/src/nvenc.c b/src/nvenc.c index 73cfeb1..0de93b4 100644 --- a/src/nvenc.c +++ b/src/nvenc.c @@ -64,6 +64,16 @@ struct cfc_encoder { /* Output bitstream buffer (один на Phase 1, потом pool) */ NV_ENC_OUTPUT_PTR output_bitstream; + /* Staging buffer — линейная CUDA-память выделенная через cuMemAlloc, + * в которую копируем NV12 кадр перед NVENC encode. Нужно потому что + * NVENC nvEncRegisterResource не принимает VMM-mapped указатели cuframes + * напрямую (NV_ENC_ERR_RESOURCE_REGISTER_FAILED) — это известное + * ограничение SDK, см. Phase 1.1 research. */ + CUdeviceptr staging_ptr; + size_t staging_size; + int staging_pitch; + NV_ENC_REGISTERED_PTR staging_regptr; + /* Кеш зарегистрированных входных буферов */ registered_resource_t registered[CFC_MAX_REGISTERED_RESOURCES]; int registered_count; @@ -275,9 +285,38 @@ int cfc_encoder_create(const cfc_encoder_config_t *cfg, cfc_encoder_t **out) fail_session); enc->output_bitstream = cb.bitstreamBuffer; + /* 6) Выделить staging buffer (legacy cuMemAlloc, 256-byte aligned pitch). + * NV12 layout: Y plane (pitch * h) + interleaved UV (pitch * h/2). */ + enc->staging_pitch = ((cfg->width + 255) & ~255); + enc->staging_size = (size_t)enc->staging_pitch * cfg->height + + (size_t)enc->staging_pitch * (cfg->height / 2); + CUresult cr = cuMemAlloc(&enc->staging_ptr, enc->staging_size); + if (cr != CUDA_SUCCESS) { + const char *es = NULL; cuGetErrorString(cr, &es); + fprintf(stderr, "[cfc/nvenc] cuMemAlloc(%zu) failed: %s\n", + enc->staging_size, es ? es : "?"); + rc = -1; goto fail_session; + } + + /* 7) Зарегистрировать staging buffer в NVENC один раз. */ + NV_ENC_REGISTER_RESOURCE sreg = { 0 }; + sreg.version = NV_ENC_REGISTER_RESOURCE_VER; + sreg.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_CUDADEVICEPTR; + sreg.width = cfg->width; + sreg.height = cfg->height; + sreg.pitch = enc->staging_pitch; + sreg.resourceToRegister = (void *)enc->staging_ptr; + sreg.bufferFormat = enc->input_format; + sreg.bufferUsage = NV_ENC_INPUT_IMAGE; + NVE_CHECK(g_nvenc_funcs.nvEncRegisterResource(enc->session, &sreg), + fail_staging); + enc->staging_regptr = sreg.registeredResource; + *out = enc; return 0; +fail_staging: + cuMemFree(enc->staging_ptr); fail_session: g_nvenc_funcs.nvEncDestroyEncoder(enc->session); fail_alloc: @@ -292,12 +331,49 @@ int cfc_encoder_encode_frame(cfc_encoder_t *enc, CUdeviceptr ptr, int pitch, { if (!enc) return -1; - NV_ENC_REGISTERED_PTR regptr = get_or_register(enc, ptr, pitch); - if (!regptr) return -1; + /* Phase 1: копируем NV12 кадр из VMM-указателя cuframes в свой staging + * буфер cuMemAlloc. Pitch источника может отличаться от staging — копируем + * по строкам. Это временное решение до Phase 1.1 (research zero-copy VMM). */ + int src_pitch = pitch; + int dst_pitch = enc->staging_pitch; + + /* Y plane: height строк */ + CUDA_MEMCPY2D y_copy = { 0 }; + y_copy.srcMemoryType = CU_MEMORYTYPE_DEVICE; + y_copy.srcDevice = ptr; + y_copy.srcPitch = src_pitch; + y_copy.dstMemoryType = CU_MEMORYTYPE_DEVICE; + y_copy.dstDevice = enc->staging_ptr; + y_copy.dstPitch = dst_pitch; + y_copy.WidthInBytes = enc->width; + y_copy.Height = enc->height; + CUresult cr = cuMemcpy2D(&y_copy); + if (cr != CUDA_SUCCESS) { + const char *es = NULL; cuGetErrorString(cr, &es); + fprintf(stderr, "[cfc/nvenc] Y plane copy failed: %s\n", es ? es : "?"); + return -1; + } + + /* UV plane: height/2 строк, начало после Y plane в src и dst */ + CUDA_MEMCPY2D uv_copy = { 0 }; + uv_copy.srcMemoryType = CU_MEMORYTYPE_DEVICE; + uv_copy.srcDevice = ptr + (size_t)src_pitch * enc->height; + uv_copy.srcPitch = src_pitch; + uv_copy.dstMemoryType = CU_MEMORYTYPE_DEVICE; + uv_copy.dstDevice = enc->staging_ptr + (size_t)dst_pitch * enc->height; + uv_copy.dstPitch = dst_pitch; + uv_copy.WidthInBytes = enc->width; + uv_copy.Height = enc->height / 2; + cr = cuMemcpy2D(&uv_copy); + if (cr != CUDA_SUCCESS) { + const char *es = NULL; cuGetErrorString(cr, &es); + fprintf(stderr, "[cfc/nvenc] UV plane copy failed: %s\n", es ? es : "?"); + return -1; + } NV_ENC_MAP_INPUT_RESOURCE mp = { 0 }; mp.version = NV_ENC_MAP_INPUT_RESOURCE_VER; - mp.registeredResource = regptr; + mp.registeredResource = enc->staging_regptr; NVENCSTATUS s = g_nvenc_funcs.nvEncMapInputResource(enc->session, &mp); if (s != NV_ENC_SUCCESS) { @@ -313,7 +389,7 @@ int cfc_encoder_encode_frame(cfc_encoder_t *enc, CUdeviceptr ptr, int pitch, pp.bufferFmt = mp.mappedBufferFmt; pp.inputWidth = enc->width; pp.inputHeight = enc->height; - pp.inputPitch = pitch; + pp.inputPitch = enc->staging_pitch; pp.pictureStruct = NV_ENC_PIC_STRUCT_FRAME; pp.inputTimeStamp = (uint64_t)pts_ns; @@ -401,6 +477,9 @@ int cfc_encoder_destroy(cfc_encoder_t *enc) pthread_mutex_unlock(&enc->registered_mu); pthread_mutex_destroy(&enc->registered_mu); + if (enc->staging_regptr) { + g_nvenc_funcs.nvEncUnregisterResource(enc->session, enc->staging_regptr); + } if (enc->output_bitstream) { g_nvenc_funcs.nvEncDestroyBitstreamBuffer(enc->session, enc->output_bitstream); @@ -408,6 +487,9 @@ int cfc_encoder_destroy(cfc_encoder_t *enc) if (enc->session) { g_nvenc_funcs.nvEncDestroyEncoder(enc->session); } + if (enc->staging_ptr) { + cuMemFree(enc->staging_ptr); + } free(enc); return 0; } diff --git a/src/source.c b/src/source.c index dff89f5..0bcea0d 100644 --- a/src/source.c +++ b/src/source.c @@ -166,15 +166,20 @@ static void *source_thread(void *arg) case CFC_SOURCE_ACTIVE: case CFC_SOURCE_STALE: { + /* cuframes требует release предыдущего frame ДО next (frame_busy + * проверяется в начале subscriber_next; см. libcuframes/src/consumer.c + * line 334). Поэтому release заранее. Это значит snapshot.ptr + * может стать невалидным к моменту чтения caller'ом — Phase 2 + * это исправит double-buffering'ом. */ + release_current_frame(src); + cuframes_frame_t *frame = NULL; - int r = cuframes_subscriber_next(src->sub, NULL, &frame, + /* consumer_stream=NULL — default stream; sync через cuMemcpy2D + * на default stream. Phase 2 — свой stream + waitEvent. */ + int r = cuframes_subscriber_next(src->sub, (void *)0, &frame, CFC_SOURCE_NEXT_TIMEOUT_MS); if (r == CUFRAMES_OK) { - /* Освобождаем предыдущий кадр (если был удержан caller'ом - * во время предыдущего snapshot — поздно, но это Phase 1 - * упрощение, см. описание сверху). */ - release_current_frame(src); src->current_frame = frame; /* Обновляем snapshot под mutex'ом. */