Phase 1 smoke test зелёный: фикс frame_busy + staging buffer

После 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 → реальное изображение с камеры
This commit is contained in:
2026-06-03 04:42:41 +01:00
parent ba68550f4c
commit eae902afb3
2 changed files with 96 additions and 9 deletions
+86 -4
View File
@@ -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;
}
+10 -5
View File
@@ -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'ом. */