Files
cuframes-composer/src/source.c
T
gx eae902afb3 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 → реальное изображение с камеры
2026-06-03 04:42:41 +01:00

300 lines
12 KiB
C

/* Обвязка над cuframes_subscriber. Реализация публичного API
* cuframes_composer/source.h.
*
* Архитектура:
* Отдельный поток на источник. cuframes_subscriber_next блокирующий, поток
* циклически зовёт его с таймаутом. На каждый успешный кадр — обновляет
* snapshot (под mutex'ом). На таймаут — переходит к проверке состояния.
*
* Snapshot pattern:
* Главный поток (тот что зовёт get_latest) не блокируется на subscribe.
* Он читает under mutex последний сохранённый CUdeviceptr + meta.
* Mutex короткий (просто копия указателя и нескольких int'ов).
*
* State machine:
* DISCONNECTED → создаём подписку → CONNECTING
* CONNECTING → подписка успешна, первый кадр получен → ACTIVE
* → подписка fail → DEAD (ждём reconnect_backoff)
* ACTIVE → последний кадр < stale_threshold_ms → остаёмся ACTIVE
* → > stale_threshold_ms → STALE
* STALE → новый кадр → ACTIVE
* → нет > dead_threshold_ms → DEAD (destroy subscriber)
* DEAD → ждём backoff (exp от reconnect_min до reconnect_max) → CONNECTING
*
* Phase 1 особенности:
* - Один источник на cfc_source_t (нет multi-source агрегации, это compose).
* - cuframes_subscriber_release вызывается при следующем get_latest либо
* при выходе. Это означает что caller не может «удерживать» snapshot
* дольше чем до следующего get_latest — должен прочитать сразу.
* В Phase 2 это уточнится при переходе на double buffering.
*/
#include "../include/cuframes_composer/source.h"
#include <cuframes/cuframes.h>
#include <errno.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
/* Внутренний таймаут блокирующего cuframes_subscriber_next. Короткий
* чтобы поток мог периодически проверять stop_flag и состояние. */
#define CFC_SOURCE_NEXT_TIMEOUT_MS 200
struct cfc_source {
cfc_source_config_t cfg;
char key_copy[64]; /* персистентная копия cfg.key */
char name_copy[32]; /* персистентная копия cfg.consumer_name */
pthread_t thread;
int thread_started;
_Atomic int stop_flag;
pthread_mutex_t state_mu;
cfc_source_state_t state;
/* Snapshot — обновляется потоком, читается через get_latest. */
cuframes_subscriber_t *sub; /* nullable — есть только в ACTIVE/STALE */
cuframes_frame_t *current_frame; /* удерживаемый frame; release при следующем next() */
cfc_source_snapshot_t snapshot;
int64_t last_frame_us; /* CLOCK_MONOTONIC момент последнего успешного кадра */
};
static int64_t now_us(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (int64_t)ts.tv_sec * 1000000 + ts.tv_nsec / 1000;
}
static void set_state(cfc_source_t *src, cfc_source_state_t s)
{
pthread_mutex_lock(&src->state_mu);
if (src->state != s) {
src->state = s;
src->snapshot.state = s;
}
pthread_mutex_unlock(&src->state_mu);
}
static void release_current_frame(cfc_source_t *src)
{
if (src->current_frame && src->sub) {
cuframes_subscriber_release(src->sub, src->current_frame);
src->current_frame = NULL;
}
}
static int try_subscribe(cfc_source_t *src)
{
cuframes_subscriber_config_t scfg = { 0 };
scfg.key = src->key_copy;
scfg.consumer_name = src->name_copy;
scfg.mode = CUFRAMES_MODE_NEWEST_ONLY;
scfg.cuda_device = src->cfg.cuda_device;
scfg.connect_timeout_ms = 2000;
int r = cuframes_subscriber_create(&scfg, &src->sub);
if (r != CUFRAMES_OK) {
fprintf(stderr,
"[cfc/source:%s] subscriber_create failed: %s\n",
src->name_copy, cuframes_strerror(r));
return -1;
}
return 0;
}
static void destroy_subscriber(cfc_source_t *src)
{
release_current_frame(src);
if (src->sub) {
cuframes_subscriber_destroy(src->sub);
src->sub = NULL;
}
}
/* Основной поток. Цикл: subscribe → next → update snapshot → проверка
* stale/dead → reconnect при необходимости. */
static void *source_thread(void *arg)
{
cfc_source_t *src = (cfc_source_t *)arg;
int64_t reconnect_backoff_ms = src->cfg.reconnect_min_ms;
while (!atomic_load(&src->stop_flag)) {
cfc_source_state_t cur;
pthread_mutex_lock(&src->state_mu);
cur = src->state;
pthread_mutex_unlock(&src->state_mu);
switch (cur) {
case CFC_SOURCE_DISCONNECTED:
case CFC_SOURCE_DEAD: {
/* Ждём backoff либо stop. */
int64_t wait_ms = reconnect_backoff_ms;
while (wait_ms > 0 && !atomic_load(&src->stop_flag)) {
int chunk = wait_ms > 100 ? 100 : (int)wait_ms;
struct timespec ts = {.tv_sec = chunk / 1000,
.tv_nsec = (long)(chunk % 1000) * 1000000L};
nanosleep(&ts, NULL);
wait_ms -= chunk;
}
if (atomic_load(&src->stop_flag)) break;
set_state(src, CFC_SOURCE_CONNECTING);
break;
}
case CFC_SOURCE_CONNECTING: {
if (try_subscribe(src) == 0) {
set_state(src, CFC_SOURCE_ACTIVE);
reconnect_backoff_ms = src->cfg.reconnect_min_ms; /* сброс backoff */
} else {
set_state(src, CFC_SOURCE_DEAD);
/* Удвоить backoff до max. */
reconnect_backoff_ms *= 2;
if (reconnect_backoff_ms > src->cfg.reconnect_max_ms) {
reconnect_backoff_ms = src->cfg.reconnect_max_ms;
}
}
break;
}
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;
/* 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) {
src->current_frame = frame;
/* Обновляем snapshot под mutex'ом. */
int32_t w = 0, h = 0;
cuframes_frame_size(frame, &w, &h);
pthread_mutex_lock(&src->state_mu);
src->snapshot.ptr = (CUdeviceptr)cuframes_frame_cuda_ptr(frame);
src->snapshot.width = w;
src->snapshot.height = h;
src->snapshot.pitch_y = cuframes_frame_pitch_y(frame);
src->snapshot.pitch_uv = cuframes_frame_pitch_uv(frame);
src->snapshot.pts_ns = cuframes_frame_pts_ns(frame);
src->snapshot.seq = cuframes_frame_seq(frame);
src->last_frame_us = now_us();
src->snapshot.last_frame_age_us = 0;
if (src->state == CFC_SOURCE_STALE) {
src->state = CFC_SOURCE_ACTIVE;
src->snapshot.state = CFC_SOURCE_ACTIVE;
}
pthread_mutex_unlock(&src->state_mu);
} else if (r == CUFRAMES_ERR_TIMEOUT || r == CUFRAMES_ERR_WOULD_BLOCK) {
/* Нет нового кадра — проверим age, может быть STALE/DEAD. */
int64_t age_ms = (now_us() - src->last_frame_us) / 1000;
if (age_ms > src->cfg.dead_threshold_ms) {
fprintf(stderr,
"[cfc/source:%s] no frame for %lldms → DEAD\n",
src->name_copy, (long long)age_ms);
destroy_subscriber(src);
set_state(src, CFC_SOURCE_DEAD);
} else if (age_ms > src->cfg.stale_threshold_ms) {
set_state(src, CFC_SOURCE_STALE);
}
} else if (r == CUFRAMES_ERR_DISCONNECTED) {
fprintf(stderr,
"[cfc/source:%s] DISCONNECTED from publisher\n",
src->name_copy);
destroy_subscriber(src);
set_state(src, CFC_SOURCE_DEAD);
} else {
fprintf(stderr,
"[cfc/source:%s] cuframes_subscriber_next failed: %s\n",
src->name_copy, cuframes_strerror(r));
destroy_subscriber(src);
set_state(src, CFC_SOURCE_DEAD);
}
break;
}
}
}
destroy_subscriber(src);
return NULL;
}
/* ── Public API ───────────────────────────────────────────────────────── */
int cfc_source_create(const cfc_source_config_t *cfg, cfc_source_t **out)
{
if (!cfg || !cfg->key || !cfg->consumer_name || !out) return -1;
cfc_source_t *src = calloc(1, sizeof(*src));
if (!src) return -1;
src->cfg = *cfg;
strncpy(src->key_copy, cfg->key, sizeof(src->key_copy) - 1);
strncpy(src->name_copy, cfg->consumer_name, sizeof(src->name_copy) - 1);
src->cfg.key = src->key_copy;
src->cfg.consumer_name = src->name_copy;
/* Дефолты */
if (src->cfg.reconnect_min_ms <= 0) src->cfg.reconnect_min_ms = 1000;
if (src->cfg.reconnect_max_ms <= 0) src->cfg.reconnect_max_ms = 30000;
if (src->cfg.stale_threshold_ms <= 0) src->cfg.stale_threshold_ms = 500;
if (src->cfg.dead_threshold_ms <= 0) src->cfg.dead_threshold_ms = 5000;
pthread_mutex_init(&src->state_mu, NULL);
src->state = CFC_SOURCE_DISCONNECTED;
src->snapshot.state = CFC_SOURCE_DISCONNECTED;
src->snapshot.last_frame_age_us = -1;
atomic_init(&src->stop_flag, 0);
if (pthread_create(&src->thread, NULL, source_thread, src) != 0) {
pthread_mutex_destroy(&src->state_mu);
free(src);
return -1;
}
src->thread_started = 1;
*out = src;
return 0;
}
int cfc_source_get_latest(cfc_source_t *src, cfc_source_snapshot_t *out)
{
if (!src || !out) return -1;
pthread_mutex_lock(&src->state_mu);
*out = src->snapshot;
if (src->last_frame_us > 0) {
out->last_frame_age_us = now_us() - src->last_frame_us;
} else {
out->last_frame_age_us = -1;
}
pthread_mutex_unlock(&src->state_mu);
return 0;
}
int cfc_source_destroy(cfc_source_t *src)
{
if (!src) return 0;
atomic_store(&src->stop_flag, 1);
if (src->thread_started) {
pthread_join(src->thread, NULL);
}
pthread_mutex_destroy(&src->state_mu);
free(src);
return 0;
}