Phase 5e revisited: visual artefacts в bottom rows 16-cell grid'а оказались
не race condition в cuframes ring buffer (как первоначальная гипотеза),
а burstiness IDR. Сложный grid (много границ между ячейками) генерит
огромные IDR (~400 КБ), которые переполняют mediamtx writeQueueSize=256
и discard'ятся → VLC видит покалеченный bitstream.
Правильное решение — canonical low-latency streaming pattern: вместо
периодических IDR использовать NVENC intra refresh. Вместо одного gigant'а
intra-кадра раз в секунду, кодируем N столбцов intra-блоков в каждом кадре.
За intra_refresh_period кадров — полный refresh. Bitstream становится
почти ровным — все кадры одинакового размера, никаких spike'ов.
Это индустриальный стандарт для low-latency: WebRTC, Twitch low-latency
mode, GeForce NOW, Zoom — все используют intra refresh без IDR.
Trade-off: новый клиент ждёт ~1 период (1 сек) для построения reference
frame. Для CCTV приемлемо. ffmpeg snapshot one-shot не работает на таких
потоках (нужен полный warmup), но VLC/TV/Frigate handles штатно.
Содержимое:
- cfc_encoder_config_t: добавлены intra_refresh + intra_refresh_period.
- nvenc.c: при enableIntraRefresh=1 устанавливается
intraRefreshPeriod/intraRefreshCnt и идуs IDR period отключается через
NVENC_INFINITE_GOPLENGTH.
- examples/grid_record: флаг --intra-refresh (период = fps).
Live-validated: rtsp://192.168.88.23:554/load16ir
- 16-cell 1080p 4×4 6Mbps intra refresh ON
- mediamtx tишина: ни одного 'reader is too slow' warning'а
- VLC connect → чистая картинка во всех 16 ячейках
- 100 кадров логи: '1 IDR' (только начальный) — после стартового никаких
больше IDR не было, ровный bitstream
Этот flag не default для случая single-source (Phase 1 simple_record) —
там IDR-based GOP всё ещё лучше (полный keyframe = быстрый connect).
Включать осознанно для multi-source grid'ов через --intra-refresh.
Результат:
46+ секунд работы, 1150 кадров, 0 drops
FPS = 25.0 СТАБИЛЬНО
GPU util = 47% (большие запасы для масштабирования)
16 active, 0 stale, 0 dead — все подписки удерживаются
ffprobe видит валидный h264 3840x2160 25/1 на mediamtx
Visual artifact в rows 2-4 (вертикальные полосы) — гипотеза:
race condition при 4 cfc_source_t подписках на одного publisher'а
(16 consumers vs 16-slot cuframes ring buffer = tight fit).
FPS не пострадало, но визуально половина ячеек испорчена.
Phase 5e-fix planned: composer должен detect одинаковые source_key
и переиспользовать один cfc_source_t для N ячеек. Это устранит
multi-consumer race и снизит memory footprint.
Подробности + снимок в docs/LOADTEST-PHASE-5e.md +
docs/phase5e-load16.jpg.
Composer настроен на 4 несуществующих cuframes-ключа. Live результат:
health: {total:4, active:0, stale:0, dead:4}
FPS: 25.2 СТАБИЛЬНО
Encoder рендерит чёрный grid с красными NO_SIGNAL подписями
+ серо-голубыми border'ами
ffprobe видит валидный h264 1920x1080 25/1 на mediamtx
Это самый жёсткий adversarial scenario — полная потеря сети камер.
Composer не падает, encoder работает на холостом ходу, RTSP-publish
непрерывен, MQTT health корректно отдаёт dead=4 (HA-алерты через
expire_after=30s).
Доказан главный production threat: обесточивание PoE-свитча /
uplink-отрезание не валит весь pipeline. TV видит NO_SIGNAL вместо
разрыва соединения, HA-оператор уведомляется.
Полный отчёт + снимок в docs/ADVERSARIAL-TEST-PHASE-5b.md +
phase5b-all-dead.jpg.
docker/Dockerfile — multi-stage build:
builder: nvidia/cuda:12.6.0-devel-ubuntu24.04 + apt build-deps + cmake
runtime: nvidia/cuda:12.6.0-runtime-ubuntu24.04 + apt runtime-deps
+ grid_record/simple_record + libcuframes.so.0 + libcuframes_composer.so.0
NVIDIA_DRIVER_CAPABILITIES=compute,utility,video в env image'а — обязательно
для NVENC dlopen libnvidia-encode.so.1.
docker/docker-compose.example.yml — образец для интеграции в
localhost-infra/hosts/R9-88.23/docker/cctv/. Два сервиса:
cfc-grid — композитор + control plane + MQTT health
cfc-grid-ffmpeg — pipe→RTSP push (использует существующий
ffmpeg-vf-cuda-grid image для совместимости glibc)
Сообщение в docs/docker/docker-compose.example.yml объясняет зависимости
(cuframes-ipc-anchor, cctv-mosquitto и т.д.) и нужный MQTT user.
Phase 6 deploy (intent перевести production):
- Создать MQTT user composer + добавить в localhost-infra mosquitto passwd
- Запустить cfc-grid параллельно с cuda-grid-pipeline (разные RTSP paths,
не конфликтуют)
- После проверки на проде — retire старый cuda-grid-pipeline
- Обновить TV-плейлисты на новый rtsp://...:554/cfc-grid
Эти шаги требуют отдельных commit'ов в localhost-infra repo с явным
user approval, поэтому здесь только prep.
Live-validated сценарий: stop cuframes-pub-back_yard, наблюдение
через ZMQ /health.
Timeline:
t=0s: 4 active, 0 stale, 0 dead baseline
t+2s: 3 active, 1 stale STALE через 500мс threshold
t+10s: 3 active, 1 dead DEAD через 5000мс threshold
FPS = 25.0 СТАБИЛЬНО
ячейка → blackout
restart pub:
t+~13s: 4 active, 0 stale, 0 dead auto-reconnect отработал
Главный threat model подтверждён: отключение источника не валит
композитор, остальные 3 ячейки продолжают работу, RTSP не прерывается.
Полный отчёт в docs/ADVERSARIAL-TEST-PHASE-5a.md.
Phase 5b/5c (multi-source disconnect, mediamtx restart, 24h memory
test, 16-source load test) — отдельные сессии.
Phase 4a — control plane через ZMQ REP socket с JSON-командами.
Позволяет операторам менять text overlay'и runtime'ом без рестарта.
Live-validated: команды set_text label-parking → "⚠ ВНИМАНИЕ ⚠"
и set_text label-gate → "Машина у ворот" отрабатывают, текст
обновляется в потоке RTSP без перерывов.
Phase 4b — MQTT health publisher через libmosquitto. Каждые 10с в
composer/<instance>/health публикуется JSON {active,stale,dead,total,
uptime_s} с retain=true. Опционально публикуется HA MQTT discovery
config — четыре сенсора (active/stale/dead/total) появляются в HA
автоматически с expire_after=30s.
Содержимое:
- include/cuframes_composer/overlay.h — cfc_overlay_set_id/get_id/get_type
для lookup'а через control plane.
- include/cuframes_composer/composer.h — cfc_composer_find_overlay(id).
- include/cuframes_composer/control.h — cfc_control_config_t (endpoint +
composer + cuda_ctx для text rebuild в worker thread).
- src/control.c — ZMQ REP socket в фоновом потоке + zmq_poll с 200мс
timeout'ом для проверки stop_flag. JSON-диспатчер для команд ping /
health / set_text / set_visible / list_overlays. cuCtxSetCurrent
на старте worker'а — без этого update_text валится на cuMemAlloc.
- include/cuframes_composer/health.h — cfc_health_config_t (host/port/
user/pass + topic_prefix/instance + interval + publish_discovery).
- src/health.c — mosquitto_connect_async + loop_start + background
thread с publish health каждые N секунд + один разовый publish_discovery
для HA.
- examples/grid_record — флаги --control tcp://0.0.0.0:5599,
--mqtt host[:port], --mqtt-instance NAME, --mqtt-user/--mqtt-pass.
Для text overlay'ев — prefix "id=NAME:" в --text задаёт control-plane ID.
CMakeLists.txt — find_library(zmq), find_library(json-c),
find_library(mosquitto) + linkage всех трёх.
Production TODO: создать отдельного MQTT user'а для composer'а вместо
переиспользования frigate creds (используется только в smoke).
Phase 3c — динамический текст через libfreetype6: открывается font
(.ttf/.otf), измеряется bounding box строки, рендерится в RGBA-атлас
с anti-aliased alpha из FT bitmap, заливается в VRAM. На каждом
кадре блитится через cfc_cugrid_blit_rgba_nv12 (kernels уже есть).
Поддержка UTF-8 через простой inline-decoder (1/2/3/4-byte).
cfc_overlay_update_text() переподдерживает re-render atlas (text /
color change) — для Phase 4 ZMQ control plane (динамическое изменение
NO SIGNAL / RECORDING / timestamp'ов).
Адаптация под GTX 1050 (Pascal) в проде:
- CMAKE_CUDA_ARCHITECTURES = 61;75;86;89;120 (Pascal + Turing +
Ampere + Ada + Blackwell, покрывает все production-кейсы)
- grid_record default снижен с 4K@10Mbps на 1080p@4Mbps. 4K требует
явных --width 3840 --height 2160 (Pascal NVENC 4K H.264 на грани).
- Ссылка на research-документ + NVENC §Input Buffers рядом со staging
buffer'ом в nvenc.c с цитатой "the client is required to use buffers
allocated using the cuMemAlloc family of APIs".
Содержимое Phase 3c:
- overlay.h — cfc_overlay_create_text + update_text + text_size.
- overlay.c — utf8_next decoder, text_measure (ascender/descender),
text_render (alpha-over blend в RGBA), text_rebuild_atlas (VRAM
cycle), full FreeType lifecycle.
- CMakeLists.txt — find_package(Freetype REQUIRED) + link
Freetype::Freetype.
- examples/grid_record — флаг --text font,size,r,g,b,x,y,text.
Live-validated на rtsp://192.168.88.23:554/cfc-grid с иконками
temp_outside.png и offline.png из cuda_grid_icons volume.
Содержимое:
- cugrid.h/cugrid.cu — cfc_cugrid_blit_rgba_nv12 (Y+UV α-blend) +
два новых kernel'я blit_rgba_y/uv (BT.709 limited-range RGB→YUV
conversion, 4:2:0 chroma 2x2 averaging).
- overlay.h — cfc_overlay_create_png + cfc_overlay_png_size +
cfc_overlay_update_png.
- overlay.c — libpng decode (paletted/gray/16-bit → 8-bit RGBA),
cuMemAlloc atlas в VRAM, cuMemcpyHtoD один раз, draw_png через
cfc_cugrid_blit_rgba_nv12.
- CMakeLists.txt — find_package(PNG REQUIRED) + PNG::PNG в link.
- examples/grid_record — флаг --icon path,x,y[,alpha] (несколько раз
можно). Атлас грузится один раз при старте.
PNG из cuda_grid_icons volume переиспользуются (offline, temp_outside,
grafana_gpu, и пр.). PNG decode'ятся одинаково — paletted, RGBA, gray.
Phase 3c (text rendering через FreeType + font atlas) — отдельный
commit. Атлас text'а тоже окажется в VRAM как RGBA через тот же
cfc_cugrid_blit_rgba_nv12 — kernels уже готовы.
Live-validated на rtsp://192.168.88.23:554/cfc-grid: цветные рамки
вокруг каждой ячейки реализованы через 4 cfc_cugrid_fill_nv12
(top/bottom/left/right) — отдельного kernel'я не понадобилось,
переиспользуем existing fill_nv12 для region α-blend.
Содержимое:
- include/cuframes_composer/overlay.h — opaque cfc_overlay_t + типы
(BORDER реализован, PNG/TEXT skeleton'ы для Phase 3b/3c).
- src/overlay.c — реализация BORDER: clamp rect в границы кадра,
выравнивание координат на чётные (4:2:0 chroma требование),
thickness clamp если 2*thickness > w/h.
- composer.c — список overlays (max 64), z-order = порядок добавления,
draw поверх grid'а перед stream sync.
- examples/grid_record — флаг --border N (толщина пикселей) добавляет
серо-голубой border (Y=180,U=120,V=110 alpha=220) для каждой
ячейки автоматически.
Phase 3b (PNG icons через stb_image + cugrid alpha_blit_rgba) и Phase
3c (text через FreeType + font atlas) — отдельные commit'ы.
Подтверждено live-тестом end-to-end в продакшен-условиях:
grid_record --out - → ffmpeg -i pipe:0 -c copy -f rtsp -rtsp_transport tcp →
mediamtx rtsp://...:554/cfc-grid → VLC на MacBook
VLC принимает поток штатно, картинка не тормозит — лучше чем у
предыдущего цельного ffmpeg-pipeline (vf-cuda-grid). 1920x1080
H.264 High@4.0 4Mbps, 4 источника active.
Изменения тривиальные: stdout не закрывается через fclose, чтобы
пайп оставался открытым для дочернего ffmpeg-процесса.
Multi-source композитор работает на 4K @ 25fps стабильно. Live-тест с
4 камерами (parking, back_yard, front_yard, gate_lpr): все 4 active,
350 кадров за 14с, 27.6 МБ H.264 файл, кадр декодируется ffmpeg'ом
с корректным 2x2 layout'ом.
Содержимое:
- include/cuframes_composer/cugrid.h — публичный API libcugrid:
cfc_cugrid_fill_nv12 (region fill с alpha blend),
cfc_cugrid_resize_nv12 (bilinear scale в rect).
- src/cugrid/cugrid.cu — извлечённые из vf_cuda_grid kernel'ы
(Y+UV fill + bilinear resize), объединены с C launcher'ом в одном
.cu файле, под LGPL-2.1+.
- include/cuframes_composer/composer.h — публичный API композитора:
cfc_composer_cell_t для layout, get_health для observability.
- src/composer.c — manager N cfc_source_t + единый NV12 output buffer
(cuMemAlloc, переиспользуется на каждом compose'е). compose_clear
fillит фон BT.709-чёрным, compose_cell делает resize ACTIVE
источника или оставляет blackout для DEAD/STALE/CONNECTING.
- examples/grid_record — Phase 2 smoke test: N --cell ключ,x,y,w,h
→ grid composer → NVENC → file.
Сборка: добавлен LANGUAGES CUDA и CMAKE_CUDA_ARCHITECTURES 89;120
(Ada + Blackwell). Compile options раздельные для C и CUDA
(-Wpedantic не подходит для .cu).
Phase 2 RTSP push отложен на отдельный commit — будет через pipe-out
к локальному ffmpeg'у, который публикует в mediamtx (вариант
утверждён в Q2 дизайн-документа).
После 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 → реальное изображение с камеры
Скелет проекта cuframes-composer (LGPL-2.1+) и MVP кодирования
одного источника в файл H.264.
Что включает Phase 1:
- LICENSE (LGPL-2.1+), README с поэтапным планом, корневой CMake
- Подмодули: cuframes v0.4 (pinned), nv-codec-headers (n12.2.72.0)
- include/cuframes_composer/source.h — публичный API источника
с явной машиной состояний (DISCONNECTED → CONNECTING → ACTIVE →
STALE → DEAD) и snapshot-паттерном для чтения без блокировки
- include/cuframes_composer/nvenc.h — публичный API кодировщика
на CUdeviceptr-вход (zero-copy через VMM-mapped указатели)
- src/nvenc_loader.{h,c} — dlopen libnvidia-encode.so.1 и инициализация
таблицы функций NVENC через NvEncodeAPICreateInstance. Идёт через
pthread_once. Сделано отдельно чтобы держать LGPL-совместимость:
проприетарный SDK не статически линкуется
- src/nvenc.c — обвязка над NVENC: open session, init encoder, кеш
registered resources, encode/lock/unlock, flush с EOS, поддержка
H.264 CBR low-latency, preset GUID p1/p4/p7
- src/source.c — обвязка над cuframes_subscriber c фоновым потоком,
exponential backoff reconnect (1с → 30с), и переходами по таймаутам
для stale/dead-детекта
- examples/simple_record — smoke-test программа: подписка на cuframes,
кодирование, запись в .h264 файл, корректное завершение по SIGINT