diff --git a/include/cuframes_composer/cpp/composer.hpp b/include/cuframes_composer/cpp/composer.hpp index 5bd93be..bf16473 100644 --- a/include/cuframes_composer/cpp/composer.hpp +++ b/include/cuframes_composer/cpp/composer.hpp @@ -23,6 +23,7 @@ #ifndef CUFRAMES_COMPOSER_CPP_COMPOSER_HPP #define CUFRAMES_COMPOSER_CPP_COMPOSER_HPP +#include "../overlay.h" /* C API — backward compat для CLI overlays */ #include "cuda_raii.hpp" #include "layout.hpp" #include "source_pool.hpp" @@ -84,6 +85,25 @@ public: return templates_; } + /* Overlays — backward compat для grid_record.c CLI (--text/--icon/--border) + * и Frigate detection_boxes. Рисуются на выходе compose_frame ПОСЛЕ Layout. + * Composer takes ownership — destroy()'ит на ~Composer(). */ + int add_overlay(cfc_overlay_t* ov); + cfc_overlay_t* find_overlay(const std::string& id) const; + + /* Health отчёт для C ABI shim. */ + struct Health { + int total = 0; + int active = 0; + int stale = 0; + int dead = 0; + }; + Health get_health() const; + + /* Manual cells — для C API без motion-mode (grid_record --cell без --motion-mode). + * Каждый вход {source_key, rect} рендерится CameraCell без template'а. */ + void set_manual_cells(const std::vector>& cells); + /* Один кадр: relayout (если нужно) + clear + render. * Возвращает NV12Ref на output (ptr действителен до следующего compose). */ NV12Ref compose_frame(); @@ -113,6 +133,13 @@ private: std::int64_t pending_first_seen_ms_ = 0; std::string committed_signature_; std::string pending_signature_; + + /* Backward-compat overlay list (CLI overlays + detbox). */ + std::vector overlays_; + + /* Manual cells — alternative режим без motion-mode (grid_record --cell). */ + std::vector> manual_cells_; + bool manual_applied_ = false; }; } // namespace cfc diff --git a/include/cuframes_composer/cpp/template_loader.hpp b/include/cuframes_composer/cpp/template_loader.hpp index 7f55c1a..90c42b1 100644 --- a/include/cuframes_composer/cpp/template_loader.hpp +++ b/include/cuframes_composer/cpp/template_loader.hpp @@ -22,6 +22,14 @@ int load_templates_from_file(const std::string& path, /* Встроенный набор fallback templates (Phase 11b base — single, quad). */ std::vector builtin_templates(); +/* Global template registry — единый источник для Composer и cfc_layout_* + * ABI shim. Заполняется builtin'ами по умолчанию; перезаписывается при + * load_templates_from_file (если был успех). Thread-safe — composer и + * control-thread читают, hot-reload пишет под lock. */ +const std::vector& current_templates(); +void set_current_templates(std::vector new_templates); +int load_into_current(const std::string& path); + } // namespace cfc #endif /* CUFRAMES_COMPOSER_CPP_TEMPLATE_LOADER_HPP */ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4cb78c2..361e0f8 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,18 +14,17 @@ set(COMPOSER_SOURCES_C source.c nvenc_loader.c nvenc.c - composer.c overlay.c control.c health.c writer.c audio.c frigate_mqtt.c - layouts.c ) -# Phase 11b — C++ ООП-модель Cell/Layout/Decoration/Composer. Параллельный -# код: грей-зона коэкзистенции с C-импл'ом, чтобы тестировать ООП-гипотезу -# через отдельный example (grid_record_cpp) без слома production grid_record. +# Phase 11b — C++ ООП-модель Cell/Layout/Decoration/Composer + ABI shim. +# Заменяет composer.c и layouts.c из Phase 10/11. Старые callers (control.c, +# frigate_mqtt.c, examples/grid_record.c) продолжают использовать те же +# cfc_composer_* и cfc_layout_* функции — они теперь обёртки над C++ ядром. set(COMPOSER_SOURCES_CPP cpp/label_decoration.cpp cpp/border_decoration.cpp @@ -36,6 +35,8 @@ set(COMPOSER_SOURCES_CPP cpp/layout.cpp cpp/template_loader.cpp cpp/composer.cpp + cpp/composer_c_api.cpp + cpp/layouts_c_api.cpp ) set(COMPOSER_SOURCES_CU cugrid/cugrid.cu diff --git a/src/composer.c b/src/composer.c deleted file mode 100644 index 618fed4..0000000 --- a/src/composer.c +++ /dev/null @@ -1,764 +0,0 @@ -/* Реализация cfc_composer_t — multi-source grid композитор. - * - * Owns: - * - N cfc_source_t (по одному на ячейку grid'а) - * - один NV12 output buffer (cuMemAlloc — staging для NVENC encoder'а) - * - statistics для health-репортов - * - * Compose-цикл: - * 1) cuMemsetD8 → быстрое черный fill всего Y plane (16=BT.709 black) - * + UV plane заполняется отдельно (128,128). - * 2) Для каждой ячейки: - * a) get_latest snapshot. - * b) ACTIVE → cfc_cugrid_resize_nv12 (src VMM → dst rect) - * c) DEAD/STALE → cfc_cugrid_fill_nv12 чёрным с alpha=255 уже сделано, - * тут лучше визуально показать что источник упал, поэтому в Phase 3 - * поверх blackout рисуется текст «NO SIGNAL» через overlay'и. - * 3) cudaStreamSynchronize → output готов. - * - * Phase 2 упрощения: - * - Sync compose на default stream. Stream pipelining — Phase 3+. - * - Без double buffering. encode и compose делаются строго последовательно. - * - * Лицензия: LGPL-2.1+ - */ - -#include "../include/cuframes_composer/composer.h" -#include "../include/cuframes_composer/cugrid.h" -#include "../include/cuframes_composer/layouts.h" -#include "../include/cuframes_composer/overlay.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -#define CFC_COMPOSER_MAX_CELLS 64 -#define CFC_COMPOSER_MAX_OVERLAYS 64 - -/* Source pool — для motion-mode. Каждая запись хранит cuframes-key, - * привязку к Frigate-камере (для motion match'а) и приоритет. */ -#define CFC_POOL_ZONE_MAX 8 -#define CFC_POOL_ZONE_NAME 32 -typedef struct cfc_pool_entry { - char cuframes_key[64]; - char frigate_camera[48]; - int priority; - cfc_source_t *source; - _Atomic int64_t last_motion_ms; - /* Optional zone-filter. */ - char required_zones[CFC_POOL_ZONE_MAX][CFC_POOL_ZONE_NAME]; - int required_zones_count; - /* Persistent text overlay "{key} prio={N}" — позиционируется в углу - * cell при relayout, hidden если камера неактивна. */ - cfc_overlay_t *label_overlay; - char label_text[96]; /* кеш строки для update_text без re-render */ -} cfc_pool_entry_t; - -struct cfc_composer { - cfc_composer_config_t cfg; - - /* Копии cells (caller владеет original config'ом). source_key копируется - * в персистентную строку чтобы cfc_source_t могла на неё указывать. */ - cfc_composer_cell_t cells[CFC_COMPOSER_MAX_CELLS]; - char cell_keys[CFC_COMPOSER_MAX_CELLS][64]; - int num_cells; - - /* Источники теперь хранятся в pool, не привязаны к cells[]. - * compose_cell ищет source через pool_find_by_key(cell.source_key). */ - - /* Output NV12 буфер: один contiguous allocation, Y plane (pitch * h) + - * UV plane (pitch * h/2). Pitch выравнен на 256 байт. */ - CUdeviceptr output_ptr; - int output_pitch_y; - int output_pitch_uv; - size_t output_size; - - /* CUDA stream для compose (Phase 2 — default stream = 0). */ - cudaStream_t stream; - - /* Overlays — в порядке добавления (= z-order). composer take ownership. */ - cfc_overlay_t *overlays[CFC_COMPOSER_MAX_OVERLAYS]; - int num_overlays; - - /* Текущий named layout (если был выставлен через set_layout). Пустая - * строка = cells заданы вручную (через --cell). */ - char current_layout[CFC_LAYOUT_MAX_NAME]; - - /* Source pool — для motion-driven layout. Все cuframes-subscriptions - * композитора живут здесь (включая те что добавились через --cell). - * compose_cell ищет source по cuframes_key. */ - cfc_pool_entry_t pool[CFC_COMPOSER_MAX_CELLS]; - int pool_count; - pthread_mutex_t pool_mu; /* для add_pool_source vs motion_pulse */ - - /* Motion-mode state. */ - int motion_mode; /* 0/1 */ - int motion_ttl_ms; /* default 45000 */ -}; - -static int64_t now_ms_mono(void) -{ - struct timespec ts; - clock_gettime(CLOCK_MONOTONIC, &ts); - return (int64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; -} - -static void compose_motion_relayout(cfc_composer_t *comp); - -/* Найти запись в pool по cuframes_key. NULL если нет. Caller держит mutex. */ -static cfc_pool_entry_t *pool_find_by_key(cfc_composer_t *comp, const char *key) -{ - if (!key) return NULL; - for (int i = 0; i < comp->pool_count; i++) { - if (!strcmp(comp->pool[i].cuframes_key, key)) return &comp->pool[i]; - } - return NULL; -} - -/* ── Helpers ──────────────────────────────────────────────────────────── */ - -static int round_up_pitch(int w) -{ - return (w + 255) & ~255; -} - -static void *cu_ptr(CUdeviceptr p) { return (void *)(uintptr_t)p; } - -/* ── Compose ──────────────────────────────────────────────────────────── */ - -static int compose_clear(cfc_composer_t *comp) -{ - /* Y plane → 16 (BT.709 black). */ - cudaError_t e = cudaMemsetAsync( - cu_ptr(comp->output_ptr), comp->cfg.bg_y, - (size_t)comp->output_pitch_y * comp->cfg.height, - comp->stream); - if (e != cudaSuccess) { - fprintf(stderr, "[cfc/composer] Y memset failed: %s\n", cudaGetErrorString(e)); - return -1; - } - - /* UV plane → нужны два значения (U=128, V=128), не один. Делаем fill - * через тот же cfc_cugrid_fill_nv12 которым fillим ячейки. Прокидываем - * alpha=255 чтобы перезатереть полностью. */ - CUdeviceptr uv = comp->output_ptr + - (size_t)comp->output_pitch_y * comp->cfg.height; - - /* Просто memset UV не подходит — там interleaved пары. Делаем fill_nv12 - * с alpha=255, тогда формула станет dst = fill * 255 / 255 = fill. */ - int rc = cfc_cugrid_fill_nv12( - (CUstream)comp->stream, - /* Y уже сделан выше — но fill_nv12 повторно fillит Y. Передаём - * y_color = bg_y, alpha=255 — get тот же результат, минор waste. - * Phase 2 acceptable, в Phase 3 разделим Y/UV fillы. */ - comp->output_ptr, comp->output_pitch_y, - uv, comp->output_pitch_uv, - 0, 0, comp->cfg.width, comp->cfg.height, - comp->cfg.bg_y, comp->cfg.bg_u, comp->cfg.bg_v, 255); - return rc; -} - -static int compose_cell(cfc_composer_t *comp, int idx) -{ - const cfc_composer_cell_t *cell = &comp->cells[idx]; - /* Layout мог обнулить cell (set_layout с меньшим num_cells) — skip. */ - if (cell->w <= 0 || cell->h <= 0) return 0; - if (!cell->source_key) return 0; - /* Source lookup в pool — motion-mode перепривязывает cells на лету, - * sources[idx] больше не валиден сам по себе. Lookup O(N), N ≤ 32. */ - cfc_pool_entry_t *p = pool_find_by_key(comp, cell->source_key); - if (!p || !p->source) return 0; - cfc_source_t *src = p->source; - - cfc_source_snapshot_t snap; - cfc_source_get_latest(src, &snap); - - if (snap.state != CFC_SOURCE_ACTIVE || snap.width <= 0) { - /* DEAD/STALE/CONNECTING — оставляем чёрный (уже clear'нут). - * Phase 3 добавит overlay «NO SIGNAL». */ - return 0; - } - - CUdeviceptr uv = comp->output_ptr + - (size_t)comp->output_pitch_y * comp->cfg.height; - /* Source NV12 layout: Y (pitch_y * height) + UV (pitch_y * height/2) - * непрерывно. Указатель на UV plane = ptr + pitch_y * height. */ - CUdeviceptr src_uv = snap.ptr + (size_t)snap.pitch_y * snap.height; - - return cfc_cugrid_resize_nv12( - (CUstream)comp->stream, - snap.ptr, snap.width, snap.height, snap.pitch_y, - src_uv, snap.pitch_uv, - comp->output_ptr, comp->output_pitch_y, - uv, comp->output_pitch_uv, - cell->x, cell->y, cell->w, cell->h); -} - -/* ── Public API ───────────────────────────────────────────────────────── */ - -int cfc_composer_create(const cfc_composer_config_t *cfg, cfc_composer_t **out) -{ - if (!cfg || !out) return -1; - if (cfg->width <= 0 || cfg->height <= 0) return -1; - if (cfg->num_cells <= 0 || cfg->num_cells > CFC_COMPOSER_MAX_CELLS) return -1; - if (!cfg->cells) return -1; - - cfc_composer_t *comp = calloc(1, sizeof(*comp)); - if (!comp) return -1; - comp->cfg = *cfg; - comp->num_cells = cfg->num_cells; - comp->stream = 0; /* default stream Phase 2 */ - pthread_mutex_init(&comp->pool_mu, NULL); - comp->motion_ttl_ms = 45000; /* default 45s (рабочий sweet spot 30-60) */ - - /* Дефолты для bg цвета (если caller не задал). */ - if (!comp->cfg.bg_y) comp->cfg.bg_y = 16; - if (!comp->cfg.bg_u) comp->cfg.bg_u = 128; - if (!comp->cfg.bg_v) comp->cfg.bg_v = 128; - - /* Сохраняем cells + копируем source_key в персистентное хранилище. */ - for (int i = 0; i < cfg->num_cells; i++) { - comp->cells[i] = cfg->cells[i]; - if (cfg->cells[i].source_key) { - strncpy(comp->cell_keys[i], cfg->cells[i].source_key, - sizeof(comp->cell_keys[i]) - 1); - comp->cells[i].source_key = comp->cell_keys[i]; - } - } - - /* Выделяем output NV12 буфер. */ - comp->output_pitch_y = round_up_pitch(cfg->width); - comp->output_pitch_uv = comp->output_pitch_y; - comp->output_size = (size_t)comp->output_pitch_y * cfg->height + - (size_t)comp->output_pitch_uv * (cfg->height / 2); - - CUresult cr = cuMemAlloc(&comp->output_ptr, comp->output_size); - if (cr != CUDA_SUCCESS) { - const char *es = NULL; cuGetErrorString(cr, &es); - fprintf(stderr, "[cfc/composer] cuMemAlloc(%zu) failed: %s\n", - comp->output_size, es ? es : "?"); - free(comp); - return -1; - } - - /* Pool: для каждой уникальной cell.source_key создаём подписку. - * Если key уже в pool (тот же source в нескольких cells) — реюзим. */ - for (int i = 0; i < comp->num_cells; i++) { - const char *key = comp->cells[i].source_key; - if (!key) continue; - if (pool_find_by_key(comp, key)) continue; /* уже добавлен */ - if (comp->pool_count >= CFC_COMPOSER_MAX_CELLS) { - fprintf(stderr, "[cfc/composer] pool overflow\n"); - break; - } - cfc_pool_entry_t *e = &comp->pool[comp->pool_count]; - strncpy(e->cuframes_key, key, sizeof(e->cuframes_key) - 1); - e->cuframes_key[sizeof(e->cuframes_key) - 1] = '\0'; - e->priority = 0; - atomic_init(&e->last_motion_ms, 0); - - char name[32]; - const char *prefix = comp->cfg.consumer_prefix; - if (!prefix || !*prefix) prefix = "composer"; - snprintf(name, sizeof(name), "%s-%d", prefix, comp->pool_count); - cfc_source_config_t scfg = { - .key = key, - .consumer_name = name, - .cuda_device = cfg->cuda_device, - .reconnect_min_ms = cfg->reconnect_min_ms, - .reconnect_max_ms = cfg->reconnect_max_ms, - .stale_threshold_ms = cfg->stale_threshold_ms, - .dead_threshold_ms = cfg->dead_threshold_ms, - }; - if (cfc_source_create(&scfg, &e->source) != 0) { - fprintf(stderr, - "[cfc/composer] cfc_source_create failed для '%s' (pool[%d])\n", - key, comp->pool_count); - e->source = NULL; /* DEAD — compose покажет blackout */ - } - comp->pool_count++; - } - - cfc_cugrid_init(); - - *out = comp; - return 0; -} - -int cfc_composer_compose(cfc_composer_t *comp, - CUdeviceptr *out_y_ptr, - int *out_pitch_y, - int *out_width, - int *out_height) -{ - if (!comp) return -1; - - /* Motion-mode пересобирает cells перед каждым кадром (no-op если выключен). */ - compose_motion_relayout(comp); - - if (compose_clear(comp) != 0) return -1; - - for (int i = 0; i < comp->num_cells; i++) { - if (compose_cell(comp, i) != 0) { - fprintf(stderr, "[cfc/composer] compose_cell %d failed\n", i); - /* Не fatal — продолжаем с остальными ячейками. */ - } - } - - /* Overlays поверх grid'а — в порядке добавления. */ - CUdeviceptr uv = comp->output_ptr + - (size_t)comp->output_pitch_y * comp->cfg.height; - for (int i = 0; i < comp->num_overlays; i++) { - if (cfc_overlay_draw(comp->overlays[i], - (CUstream)comp->stream, - comp->output_ptr, comp->output_pitch_y, - uv, comp->output_pitch_uv, - comp->cfg.width, comp->cfg.height) != 0) { - fprintf(stderr, "[cfc/composer] overlay %d draw failed\n", i); - /* Не fatal. */ - } - } - - cudaError_t e = cudaStreamSynchronize(comp->stream); - if (e != cudaSuccess) { - fprintf(stderr, "[cfc/composer] stream sync failed: %s\n", - cudaGetErrorString(e)); - return -1; - } - - if (out_y_ptr) *out_y_ptr = comp->output_ptr; - if (out_pitch_y) *out_pitch_y = comp->output_pitch_y; - if (out_width) *out_width = comp->cfg.width; - if (out_height) *out_height = comp->cfg.height; - return 0; -} - -int cfc_composer_add_overlay(cfc_composer_t *comp, cfc_overlay_t *ov) -{ - if (!comp || !ov) return -1; - if (comp->num_overlays >= CFC_COMPOSER_MAX_OVERLAYS) { - fprintf(stderr, "[cfc/composer] overlay limit %d reached\n", - CFC_COMPOSER_MAX_OVERLAYS); - return -1; - } - comp->overlays[comp->num_overlays++] = ov; - return 0; -} - -cfc_overlay_t *cfc_composer_find_overlay(cfc_composer_t *comp, const char *id) -{ - if (!comp || !id) return NULL; - for (int i = 0; i < comp->num_overlays; i++) { - const char *oid = cfc_overlay_get_id(comp->overlays[i]); - if (oid && !strcmp(oid, id)) return comp->overlays[i]; - } - return NULL; -} - -int cfc_composer_set_layout(cfc_composer_t *comp, const char *layout_name) -{ - if (!comp || !layout_name) return -1; - /* В motion-mode set_layout игнорируется — relayout управляется - * автоматически из compose_motion_relayout. */ - if (comp->motion_mode) { - fprintf(stderr, "[cfc/composer] set_layout('%s') ignored: motion_mode active\n", - layout_name); - return -1; - } - const cfc_layout_t *lay = cfc_layout_find(layout_name); - if (!lay) { - fprintf(stderr, "[cfc/composer] unknown layout '%s'\n", layout_name); - return -1; - } - - int W = comp->cfg.width, H = comp->cfg.height; - int n_apply = lay->nb_cells; - if (n_apply > CFC_COMPOSER_MAX_CELLS) n_apply = CFC_COMPOSER_MAX_CELLS; - if (n_apply > comp->num_cells) n_apply = comp->num_cells; - /* Микро-сетка → pixel coords для каждой cell. Source_key привязок не - * меняем (Step 2+ добавит role/order распределение). */ - for (int i = 0; i < n_apply; i++) { - int x, y, w, h; - cfc_layout_to_pixels(&lay->cells[i], W, H, &x, &y, &w, &h); - comp->cells[i].x = x; - comp->cells[i].y = y; - comp->cells[i].w = w; - comp->cells[i].h = h; - } - /* Cells сверх layout->nb_cells — обнуляем, чтобы не рисовались. */ - for (int i = n_apply; i < comp->num_cells; i++) { - comp->cells[i].w = 0; - comp->cells[i].h = 0; - } - - strncpy(comp->current_layout, lay->name, sizeof(comp->current_layout) - 1); - comp->current_layout[sizeof(comp->current_layout) - 1] = '\0'; - fprintf(stderr, "[cfc/composer] layout='%s' (%d active cells, %d sources)\n", - lay->name, n_apply, comp->num_cells); - return 0; -} - -const char *cfc_composer_current_layout(cfc_composer_t *comp) -{ - if (!comp) return NULL; - return comp->current_layout[0] ? comp->current_layout : NULL; -} - -/* ── Motion-driven layout ────────────────────────────────────────────── */ - -/* Распарсить colon-separated zones list в entry. */ -static void parse_zones(cfc_pool_entry_t *e, const char *zones) -{ - e->required_zones_count = 0; - if (!zones || !*zones) return; - const char *p = zones; - while (p && *p && e->required_zones_count < CFC_POOL_ZONE_MAX) { - const char *sep = strchr(p, ':'); - int len = sep ? (int)(sep - p) : (int)strlen(p); - if (len > CFC_POOL_ZONE_NAME - 1) len = CFC_POOL_ZONE_NAME - 1; - memcpy(e->required_zones[e->required_zones_count], p, len); - e->required_zones[e->required_zones_count][len] = '\0'; - e->required_zones_count++; - p = sep ? sep + 1 : NULL; - } -} - -int cfc_composer_add_pool_source(cfc_composer_t *comp, - const char *cuframes_key, - const char *frigate_camera, - int priority, - const char *required_zones) -{ - if (!comp || !cuframes_key) return -1; - pthread_mutex_lock(&comp->pool_mu); - - cfc_pool_entry_t *e = pool_find_by_key(comp, cuframes_key); - if (e) { - /* Уже в pool (был добавлен из --cell). Просто перебиваем - * frigate_camera + priority + zones. */ - if (frigate_camera) { - strncpy(e->frigate_camera, frigate_camera, sizeof(e->frigate_camera) - 1); - e->frigate_camera[sizeof(e->frigate_camera) - 1] = '\0'; - } - e->priority = priority; - parse_zones(e, required_zones); - pthread_mutex_unlock(&comp->pool_mu); - return 0; - } - if (comp->pool_count >= CFC_COMPOSER_MAX_CELLS) { - pthread_mutex_unlock(&comp->pool_mu); - return -1; - } - e = &comp->pool[comp->pool_count]; - strncpy(e->cuframes_key, cuframes_key, sizeof(e->cuframes_key) - 1); - e->cuframes_key[sizeof(e->cuframes_key) - 1] = '\0'; - if (frigate_camera) { - strncpy(e->frigate_camera, frigate_camera, sizeof(e->frigate_camera) - 1); - e->frigate_camera[sizeof(e->frigate_camera) - 1] = '\0'; - } - e->priority = priority; - parse_zones(e, required_zones); - atomic_init(&e->last_motion_ms, 0); - - char name[32]; - const char *prefix = comp->cfg.consumer_prefix; - if (!prefix || !*prefix) prefix = "composer"; - snprintf(name, sizeof(name), "%s-%d", prefix, comp->pool_count); - cfc_source_config_t scfg = { - .key = e->cuframes_key, - .consumer_name = name, - .cuda_device = comp->cfg.cuda_device, - .reconnect_min_ms = comp->cfg.reconnect_min_ms, - .reconnect_max_ms = comp->cfg.reconnect_max_ms, - .stale_threshold_ms = comp->cfg.stale_threshold_ms, - .dead_threshold_ms = comp->cfg.dead_threshold_ms, - }; - if (cfc_source_create(&scfg, &e->source) != 0) { - fprintf(stderr, "[cfc/composer] add_pool_source: subscribe '%s' failed\n", - cuframes_key); - e->source = NULL; - } - - /* Persistent text overlay для подписи cell — позиция выставляется в - * compose_motion_relayout (visible=1 + x/y); неактивные камеры — visible=0. - * Font hardcoded под production volume `/fonts/DejaVuSans-Bold.ttf`. */ - snprintf(e->label_text, sizeof(e->label_text), "%s prio=%d", cuframes_key, priority); - cfc_overlay_text_config_t tc = { - .font_path = "/fonts/DejaVuSans-Bold.ttf", - .text = e->label_text, - .pixel_size = 22, - .x = 0, .y = 0, - .r = 255, .g = 220, .b = 64, /* жёлто-оранжевый: читается на любом фоне */ - .extra_alpha = 255, - .visible = 0, - }; - if (cfc_overlay_create_text(&tc, &e->label_overlay) == 0) { - cfc_composer_add_overlay(comp, e->label_overlay); - } else { - fprintf(stderr, "[cfc/composer] label overlay для '%s' не создан\n", cuframes_key); - e->label_overlay = NULL; - } - - comp->pool_count++; - pthread_mutex_unlock(&comp->pool_mu); - fprintf(stderr, "[cfc/composer] pool+ '%s' (frigate=%s prio=%d) total=%d\n", - cuframes_key, frigate_camera ? frigate_camera : "-", - priority, comp->pool_count); - return 0; -} - -int cfc_composer_set_motion_mode(cfc_composer_t *comp, int on, int ttl_ms) -{ - if (!comp) return -1; - comp->motion_mode = on ? 1 : 0; - if (ttl_ms > 0) comp->motion_ttl_ms = ttl_ms; - fprintf(stderr, "[cfc/composer] motion_mode=%d ttl=%dms pool=%d\n", - comp->motion_mode, comp->motion_ttl_ms, comp->pool_count); - return 0; -} - -int cfc_composer_get_motion_mode(cfc_composer_t *comp) -{ - return comp ? comp->motion_mode : 0; -} - -/* Сверить current_zones с required_zones записи pool'а. */ -static int zones_match(const cfc_pool_entry_t *e, - const char *const *current_zones, int n) -{ - if (e->required_zones_count == 0) return 1; /* фильтр выключен */ - if (n <= 0 || !current_zones) return 0; - for (int i = 0; i < n; i++) { - if (!current_zones[i]) continue; - for (int j = 0; j < e->required_zones_count; j++) { - if (!strcmp(current_zones[i], e->required_zones[j])) return 1; - } - } - return 0; -} - -int cfc_composer_motion_pulse(cfc_composer_t *comp, - const char *frigate_camera, - const char *const *current_zones, - int n_zones) -{ - if (!comp || !frigate_camera) return -1; - int found = 0; - pthread_mutex_lock(&comp->pool_mu); - int64_t now = now_ms_mono(); - for (int i = 0; i < comp->pool_count; i++) { - if (!comp->pool[i].frigate_camera[0]) continue; - if (strcmp(comp->pool[i].frigate_camera, frigate_camera) != 0) continue; - if (!zones_match(&comp->pool[i], current_zones, n_zones)) continue; - atomic_store(&comp->pool[i].last_motion_ms, now); - found = 1; - } - pthread_mutex_unlock(&comp->pool_mu); - return found ? 0 : -1; -} - -/* Best-fit selection: минимальный template c nb_camera_cells >= need. - * При ties побеждает выше priority. Если ничего не подходит — самый большой. */ -static const cfc_layout_t *pick_best_fit(int need) -{ - int n_layouts = 0; - const cfc_layout_t *all = cfc_layout_all(&n_layouts); - if (n_layouts == 0) return NULL; - - const cfc_layout_t *best = NULL; - int best_waste = -1; - int best_prio = -1; - for (int i = 0; i < n_layouts; i++) { - const cfc_layout_t *l = &all[i]; - if (l->nb_camera_cells < need) continue; - int waste = l->nb_camera_cells - need; - if (best == NULL || waste < best_waste || - (waste == best_waste && l->priority > best_prio)) { - best = l; - best_waste = waste; - best_prio = l->priority; - } - } - if (best) return best; - - /* Overflow: ни один не подходит. Берём с max nb_camera_cells. */ - best = &all[0]; - for (int i = 1; i < n_layouts; i++) { - if (all[i].nb_camera_cells > best->nb_camera_cells) best = &all[i]; - } - return best; -} - -/* Motion-mode relayout — Phase 11 Step 2. - * - * Алгоритм: - * 1. active = {pool[i] : (now - last_motion_ms) < ttl} - * 2. sort active by priority DESC - * 3. template = pick_best_fit(|active|) - * 4. распределить active по template.cells[role=CAMERA] в порядке order ASC - * 5. лишние cells.w = 0 - * 6. idle (|active|=0): tpl_1 + top-priority из pool - * - * Hysteresis, DEAD-exclusion и widget rendering — следующие step'ы. */ -static void compose_motion_relayout(cfc_composer_t *comp) -{ - if (!comp->motion_mode) return; - if (comp->pool_count == 0) return; - - int64_t now = now_ms_mono(); - int active_idx[CFC_COMPOSER_MAX_CELLS]; - int active_prio[CFC_COMPOSER_MAX_CELLS]; - int n_active = 0; - - pthread_mutex_lock(&comp->pool_mu); - for (int i = 0; i < comp->pool_count; i++) { - int64_t last = atomic_load(&comp->pool[i].last_motion_ms); - if (last == 0) continue; - if (now - last > comp->motion_ttl_ms) continue; - active_idx[n_active] = i; - active_prio[n_active] = comp->pool[i].priority; - n_active++; - } - - /* Idle: 0 active → tpl_1 + top-priority pool entry. */ - if (n_active == 0) { - int best = 0; - for (int i = 1; i < comp->pool_count; i++) { - if (comp->pool[i].priority > comp->pool[best].priority) best = i; - } - active_idx[0] = best; - active_prio[0] = comp->pool[best].priority; - n_active = 1; - } - pthread_mutex_unlock(&comp->pool_mu); - - /* Insertion sort by priority DESC (stable). */ - for (int i = 1; i < n_active; i++) { - int ki = active_idx[i], kp = active_prio[i]; - int j = i - 1; - while (j >= 0 && active_prio[j] < kp) { - active_idx[j + 1] = active_idx[j]; - active_prio[j + 1] = active_prio[j]; - j--; - } - active_idx[j + 1] = ki; - active_prio[j + 1] = kp; - } - - const cfc_layout_t *lay = pick_best_fit(n_active); - if (!lay) return; - - /* Если active > template — обрезаем (lowest-priority вылетают). */ - int slots = lay->nb_camera_cells; - if (n_active > slots) n_active = slots; - - /* Сначала спрятать все labels — активные включим ниже. */ - for (int i = 0; i < comp->pool_count; i++) { - if (!comp->pool[i].label_overlay) continue; - cfc_overlay_text_config_t hide = { - .font_path = "/fonts/DejaVuSans-Bold.ttf", - .text = comp->pool[i].label_text, /* тот же текст — без re-render */ - .pixel_size = 22, - .r = 255, .g = 220, .b = 64, - .extra_alpha = 255, .visible = 0, - }; - cfc_overlay_update_text(comp->pool[i].label_overlay, &hide); - } - - /* Распределить активные по camera-cells (в порядке order ASC). */ - int W = comp->cfg.width, H = comp->cfg.height; - int placed = 0; - for (int order = 0; order < slots && placed < n_active; order++) { - for (int i = 0; i < lay->nb_cells; i++) { - const cfc_cell_t *c = &lay->cells[i]; - if (c->role != CFC_CELL_CAMERA) continue; - if (c->order != order) continue; - - int x, y, w, h; - cfc_layout_to_pixels(c, W, H, &x, &y, &w, &h); - comp->cells[placed].source_key = comp->pool[active_idx[placed]].cuframes_key; - comp->cells[placed].x = x; comp->cells[placed].y = y; - comp->cells[placed].w = w; comp->cells[placed].h = h; - - /* Включить label в левом-верхнем углу cell. */ - cfc_pool_entry_t *pe = &comp->pool[active_idx[placed]]; - if (pe->label_overlay) { - cfc_overlay_text_config_t show = { - .font_path = "/fonts/DejaVuSans-Bold.ttf", - .text = pe->label_text, /* тот же текст — без re-render */ - .pixel_size = 22, - .x = x + 10, .y = y + 8, - .r = 255, .g = 220, .b = 64, - .extra_alpha = 255, .visible = 1, - }; - cfc_overlay_update_text(pe->label_overlay, &show); - } - placed++; - break; - } - } - /* Лишние cells composer'а (за пределами placed) — обнуляем. */ - for (int i = placed; i < CFC_COMPOSER_MAX_CELLS; i++) { - comp->cells[i].w = 0; - comp->cells[i].h = 0; - } - comp->num_cells = placed; - - /* Сигнатура для лога — меняется при смене template или active set. */ - static char last_signature[256]; - char sig[256]; - int off = snprintf(sig, sizeof(sig), "%s|", lay->name); - for (int i = 0; i < placed && off < (int)sizeof(sig) - 1; i++) { - int n = snprintf(sig + off, sizeof(sig) - off, "%s,", - comp->pool[active_idx[i]].cuframes_key); - if (n <= 0) break; - off += n; - } - if (strcmp(sig, last_signature) != 0) { - strncpy(last_signature, sig, sizeof(last_signature) - 1); - last_signature[sizeof(last_signature) - 1] = '\0'; - fprintf(stderr, "[cfc/composer] motion-template='%s' active=%d : %s\n", - lay->name, placed, sig); - } -} - -int cfc_composer_get_health(cfc_composer_t *comp, cfc_composer_health_t *out) -{ - if (!comp || !out) return -1; - memset(out, 0, sizeof(*out)); - out->total = comp->pool_count; - for (int i = 0; i < comp->pool_count; i++) { - if (!comp->pool[i].source) { - out->dead++; - continue; - } - cfc_source_snapshot_t snap; - cfc_source_get_latest(comp->pool[i].source, &snap); - switch (snap.state) { - case CFC_SOURCE_ACTIVE: out->active++; break; - case CFC_SOURCE_STALE: out->stale++; break; - default: out->dead++; break; - } - } - return 0; -} - -int cfc_composer_destroy(cfc_composer_t *comp) -{ - if (!comp) return 0; - for (int i = 0; i < comp->pool_count; i++) { - if (comp->pool[i].source) cfc_source_destroy(comp->pool[i].source); - } - for (int i = 0; i < comp->num_overlays; i++) { - cfc_overlay_destroy(comp->overlays[i]); - } - pthread_mutex_destroy(&comp->pool_mu); - if (comp->output_ptr) cuMemFree(comp->output_ptr); - free(comp); - return 0; -} diff --git a/src/cpp/composer.cpp b/src/cpp/composer.cpp index 7d84fcf..330e4fe 100644 --- a/src/cpp/composer.cpp +++ b/src/cpp/composer.cpp @@ -34,25 +34,69 @@ Composer::Composer(const ComposerConfig& cfg) : cfg_(cfg) cfc_cugrid_init(); - /* Templates. */ + /* Templates: грузим через глобальный registry, чтобы hot-reload через + * ABI shim (cfc_layout_load_file из любого треда) был виден компосеру + * на следующем кадре. */ if (!cfg_.templates_path.empty()) { - load_templates(cfg_.templates_path); + load_into_current(cfg_.templates_path); } - if (templates_.empty()) { - templates_ = builtin_templates(); - std::fprintf(stderr, "[cfc/composer] using built-in templates: %zu\n", - templates_.size()); + /* В Composer держим только snapshot — реальный source истины = + * current_templates(). Снимок обновляется в pick_best_fit на лету. */ + templates_ = current_templates(); + std::fprintf(stderr, "[cfc/composer] templates loaded: %zu (path='%s')\n", + templates_.size(), cfg_.templates_path.c_str()); +} + +Composer::~Composer() +{ + for (auto* ov : overlays_) { + if (ov) cfc_overlay_destroy(ov); } } -Composer::~Composer() = default; +int Composer::add_overlay(cfc_overlay_t* ov) +{ + if (!ov) return -1; + overlays_.push_back(ov); + return 0; +} + +cfc_overlay_t* Composer::find_overlay(const std::string& id) const +{ + for (auto* ov : overlays_) { + const char* oid = cfc_overlay_get_id(ov); + if (oid && id == oid) return ov; + } + return nullptr; +} + +Composer::Health Composer::get_health() const +{ + Health h{}; + auto& pool_ref = const_cast(pool_); + pool_ref.for_each([&](PoolEntry& e) { + h.total++; + cfc_source_state_t st = e.state(); + switch (st) { + case CFC_SOURCE_ACTIVE: h.active++; break; + case CFC_SOURCE_STALE: h.stale++; break; + default: h.dead++; break; + } + }); + return h; +} + +void Composer::set_manual_cells(const std::vector>& cells) +{ + manual_cells_ = cells; + manual_applied_ = false; /* compose_frame применит */ +} int Composer::load_templates(const std::string& path) { - std::vector v; - int r = load_templates_from_file(path, v); + int r = load_into_current(path); if (r > 0) { - templates_ = std::move(v); + templates_ = current_templates(); } return r; } @@ -75,9 +119,10 @@ bool Composer::set_layout(const std::string& name) name.c_str()); return false; } - auto it = std::find_if(templates_.begin(), templates_.end(), + const auto& reg = current_templates(); + auto it = std::find_if(reg.begin(), reg.end(), [&](const LayoutTemplate& t) { return t.name == name; }); - if (it == templates_.end()) { + if (it == reg.end()) { std::fprintf(stderr, "[cfc/composer] unknown template '%s'\n", name.c_str()); return false; } @@ -94,10 +139,13 @@ bool Composer::set_layout(const std::string& name) const LayoutTemplate* Composer::pick_best_fit(int need) const { + /* Читаем global registry — hot-reload через cfc_layout_load_file + * подхватывается на следующем кадре без relink composer'а. */ + const auto& reg = current_templates(); const LayoutTemplate* best = nullptr; int best_waste = -1; int best_prio = -1; - for (auto& t : templates_) { + for (auto& t : reg) { int n = t.nb_camera_cells(); if (n < need) continue; int waste = n - need; @@ -111,7 +159,7 @@ const LayoutTemplate* Composer::pick_best_fit(int need) const if (best) return best; /* Overflow → largest. */ - for (auto& t : templates_) { + for (auto& t : reg) { if (!best || t.nb_camera_cells() > best->nb_camera_cells()) best = &t; } return best; @@ -157,7 +205,7 @@ std::string Composer::build_signature(const std::string& tpl_name, void Composer::maybe_relayout() { if (!motion_mode_) return; - if (templates_.empty()) return; + if (current_templates().empty()) return; auto active = collect_active(); const LayoutTemplate* tpl = pick_best_fit(static_cast(active.size())); @@ -228,6 +276,36 @@ void Composer::maybe_relayout() NV12Ref Composer::compose_frame() { + /* Если manual cells заданы (через C API без motion-mode) — apply один раз. */ + if (!motion_mode_ && !manual_cells_.empty() && !manual_applied_) { + /* Build LayoutTemplate из manual cells как inline. */ + LayoutTemplate t; + t.name = "manual"; + for (std::size_t i = 0; i < manual_cells_.size(); i++) { + CellTemplate c; + /* manual_cells_ хранит pixel Rect — для LayoutTemplate переводим + * обратно в микроячейки. Округление в большую сторону безопасно. */ + const Rect& r = manual_cells_[i].second; + c.col = r.x * kGridCols / cfg_.width; + c.row = r.y * kGridRows / cfg_.height; + c.cs = (r.w * kGridCols + cfg_.width - 1) / cfg_.width; + c.rs = (r.h * kGridRows + cfg_.height - 1) / cfg_.height; + c.role = CellRole::Camera; + c.order = static_cast(i); + t.cells.push_back(std::move(c)); + } + /* Build active list из pool entries по cuframes_key. */ + std::vector snap; + for (auto& kv : manual_cells_) { + PoolEntry* e = pool_.by_key(kv.first); + snap.push_back(e); /* nullptr → BlankCell */ + } + layout_.apply(t, snap, cfg_.width, cfg_.height); + committed_signature_ = build_signature(t.name, snap); + committed_at_ms_ = now_ms_mono(); + manual_applied_ = true; + } + maybe_relayout(); /* clear */ @@ -249,6 +327,14 @@ NV12Ref Composer::compose_frame() dst.frame_h = cfg_.height; layout_.render(reinterpret_cast(stream_), dst); + + /* Backward-compat overlays (CLI text/icon, detbox) — поверх Layout. */ + for (auto* ov : overlays_) { + if (!ov) continue; + cfc_overlay_draw(ov, reinterpret_cast(stream_), + y, pitch_y_, uv, pitch_uv_, + cfg_.width, cfg_.height); + } return dst; } diff --git a/src/cpp/composer_c_api.cpp b/src/cpp/composer_c_api.cpp new file mode 100644 index 0000000..49d02f7 --- /dev/null +++ b/src/cpp/composer_c_api.cpp @@ -0,0 +1,197 @@ +/* composer_c_api — extern "C" ABI shim для C++ Composer (Phase 11b). + * + * Существующие callers (control.c, frigate_mqtt.c, examples/grid_record.c) + * продолжают использовать prototype cfc_composer_* без изменений. Здесь + * каждый из них транслируется в вызов соответствующего метода cfc::Composer. + * + * Opaque handle cfc_composer_t = cfc::Composer (через reinterpret_cast). + * + * Лицензия: LGPL-2.1+ + */ + +#include "../../include/cuframes_composer/composer.h" +#include "../../include/cuframes_composer/cpp/composer.hpp" + +#include +#include +#include +#include +#include + +namespace { + +inline cfc::Composer* as_cpp(cfc_composer_t* h) +{ + return reinterpret_cast(h); +} +inline cfc_composer_t* as_c(cfc::Composer* c) +{ + return reinterpret_cast(c); +} + +} // namespace + +extern "C" { + +int cfc_composer_create(const cfc_composer_config_t* cfg, cfc_composer_t** out) +{ + if (!cfg || !out) return -1; + if (cfg->width <= 0 || cfg->height <= 0) return -1; + + cfc::ComposerConfig cpp_cfg; + cpp_cfg.width = cfg->width; + cpp_cfg.height = cfg->height; + cpp_cfg.cuda_device = cfg->cuda_device; + if (cfg->bg_y) cpp_cfg.bg_y = cfg->bg_y; + if (cfg->bg_u) cpp_cfg.bg_u = cfg->bg_u; + if (cfg->bg_v) cpp_cfg.bg_v = cfg->bg_v; + + auto* comp = new (std::nothrow) cfc::Composer(cpp_cfg); + if (!comp || !comp->ok()) { + delete comp; + return -1; + } + + /* Если caller передал cells через --cell → запоминаем как manual cells. + * Apply отложен до compose_frame (тогда pool уже наполнен через + * add_pool_source). */ + if (cfg->cells && cfg->num_cells > 0) { + std::vector> manual; + for (int i = 0; i < cfg->num_cells; i++) { + const auto& c = cfg->cells[i]; + if (!c.source_key) continue; + cfc::Rect r; + r.x = c.x; r.y = c.y; r.w = c.w; r.h = c.h; + manual.emplace_back(std::string(c.source_key), r); + } + comp->set_manual_cells(manual); + /* Также добавляем источник в pool автоматически — иначе lookup + * не найдёт его. Priority=0, frigate=none, zones=[]. */ + cfc::SourcePool::SubscribeOpts opts; + if (cfg->consumer_prefix && *cfg->consumer_prefix) + opts.consumer_prefix = cfg->consumer_prefix; + if (cfg->reconnect_min_ms) opts.reconnect_min_ms = cfg->reconnect_min_ms; + if (cfg->reconnect_max_ms) opts.reconnect_max_ms = cfg->reconnect_max_ms; + if (cfg->stale_threshold_ms) opts.stale_threshold_ms = cfg->stale_threshold_ms; + if (cfg->dead_threshold_ms) opts.dead_threshold_ms = cfg->dead_threshold_ms; + for (const auto& kv : manual) { + comp->pool().add(kv.first, "", 0, {}, opts); + } + } + + std::fprintf(stderr, "[cfc/composer] C++ ABI shim, %dx%d, %d manual cells\n", + cfg->width, cfg->height, cfg->num_cells); + *out = as_c(comp); + return 0; +} + +int cfc_composer_compose(cfc_composer_t* h, + CUdeviceptr* out_y_ptr, + int* out_pitch_y, + int* out_width, + int* out_height) +{ + if (!h) return -1; + cfc::NV12Ref ref = as_cpp(h)->compose_frame(); + if (out_y_ptr) *out_y_ptr = ref.y_ptr; + if (out_pitch_y) *out_pitch_y = ref.pitch_y; + if (out_width) *out_width = ref.frame_w; + if (out_height) *out_height = ref.frame_h; + return 0; +} + +int cfc_composer_add_overlay(cfc_composer_t* h, cfc_overlay_t* ov) +{ + if (!h) return -1; + return as_cpp(h)->add_overlay(ov); +} + +cfc_overlay_t* cfc_composer_find_overlay(cfc_composer_t* h, const char* id) +{ + if (!h || !id) return nullptr; + return as_cpp(h)->find_overlay(id); +} + +int cfc_composer_set_layout(cfc_composer_t* h, const char* layout_name) +{ + if (!h || !layout_name) return -1; + return as_cpp(h)->set_layout(layout_name) ? 0 : -1; +} + +const char* cfc_composer_current_layout(cfc_composer_t* h) +{ + if (!h) return nullptr; + const std::string& n = as_cpp(h)->current_layout_name(); + return n.empty() ? nullptr : n.c_str(); +} + +int cfc_composer_add_pool_source(cfc_composer_t* h, + const char* cuframes_key, + const char* frigate_camera, + int priority, + const char* required_zones) +{ + if (!h || !cuframes_key) return -1; + std::vector zones; + if (required_zones && *required_zones) { + std::string cur; + for (const char* p = required_zones; *p; p++) { + if (*p == ':') { if (!cur.empty()) zones.push_back(cur); cur.clear(); } + else cur.push_back(*p); + } + if (!cur.empty()) zones.push_back(cur); + } + cfc::SourcePool::SubscribeOpts opts; + int idx = as_cpp(h)->pool().add(cuframes_key, + frigate_camera ? frigate_camera : "", + priority, zones, opts); + std::fprintf(stderr, "[cfc/composer] pool+ '%s' (frigate=%s prio=%d zones=%zu) → idx=%d\n", + cuframes_key, frigate_camera ? frigate_camera : "-", + priority, zones.size(), idx); + return idx >= 0 ? 0 : -1; +} + +int cfc_composer_set_motion_mode(cfc_composer_t* h, int on, int ttl_ms) +{ + if (!h) return -1; + as_cpp(h)->set_motion_mode(on != 0, ttl_ms); + return 0; +} + +int cfc_composer_get_motion_mode(cfc_composer_t* h) +{ + return h ? (as_cpp(h)->motion_mode() ? 1 : 0) : 0; +} + +int cfc_composer_motion_pulse(cfc_composer_t* h, + const char* frigate_camera, + const char* const* current_zones, + int n_zones) +{ + if (!h || !frigate_camera) return -1; + std::vector zones; + for (int i = 0; i < n_zones; i++) { + if (current_zones[i]) zones.emplace_back(current_zones[i]); + } + as_cpp(h)->pool().motion_pulse(frigate_camera, zones); + return 0; +} + +int cfc_composer_get_health(cfc_composer_t* h, cfc_composer_health_t* out) +{ + if (!h || !out) return -1; + auto hh = as_cpp(h)->get_health(); + out->total = hh.total; + out->active = hh.active; + out->stale = hh.stale; + out->dead = hh.dead; + return 0; +} + +int cfc_composer_destroy(cfc_composer_t* h) +{ + if (h) delete as_cpp(h); + return 0; +} + +} // extern "C" diff --git a/src/cpp/layouts_c_api.cpp b/src/cpp/layouts_c_api.cpp new file mode 100644 index 0000000..e6008ec --- /dev/null +++ b/src/cpp/layouts_c_api.cpp @@ -0,0 +1,136 @@ +/* layouts_c_api — extern "C" ABI shim над template_loader (Phase 11b). + * + * Сохраняет совместимость с control.c::cmd_list_layouts/_get_layout/_set_layout + * через старый интерфейс cfc_layout_find / cfc_layout_all. + * + * Стратегия: static cache из cfc_layout_t structs, заполняется при + * load_file/reload. cfc_layout_find/_all возвращают указатели в этот cache. + * + * Лицензия: LGPL-2.1+ + */ + +#include "../../include/cuframes_composer/layouts.h" +#include "../../include/cuframes_composer/cpp/template.hpp" +#include "../../include/cuframes_composer/cpp/template_loader.hpp" + +#include +#include +#include +#include +#include + +namespace { + +static std::mutex g_mu; +static std::vector g_c_cache; +static std::string g_loaded_path; +static std::vector g_cpp_cache; + +void rebuild_c_cache_locked() +{ + g_c_cache.clear(); + g_c_cache.reserve(g_cpp_cache.size()); + for (const auto& t : g_cpp_cache) { + cfc_layout_t l{}; + std::strncpy(l.name, t.name.c_str(), sizeof(l.name) - 1); + l.priority = t.priority; + l.nb_cells = static_cast(t.cells.size()); + if (l.nb_cells > CFC_LAYOUT_MAX_CELLS) l.nb_cells = CFC_LAYOUT_MAX_CELLS; + l.nb_camera_cells = 0; + for (int i = 0; i < l.nb_cells; i++) { + const auto& c = t.cells[i]; + l.cells[i].col = c.col; + l.cells[i].row = c.row; + l.cells[i].cs = c.cs; + l.cells[i].rs = c.rs; + l.cells[i].role = (c.role == cfc::CellRole::Widget) + ? CFC_CELL_WIDGET : CFC_CELL_CAMERA; + l.cells[i].order = c.order; + std::strncpy(l.cells[i].widget, c.widget.c_str(), + sizeof(l.cells[i].widget) - 1); + if (l.cells[i].role == CFC_CELL_CAMERA) l.nb_camera_cells++; + } + g_c_cache.push_back(l); + } +} + +void ensure_loaded_locked() +{ + /* Источник истины = global registry; кеш C-структур пересинхронизируется + * каждый раз когда состав изменился (поэтому простая проверка empty + * не годится — может появиться обновление через load_file). */ + g_cpp_cache = cfc::current_templates(); + rebuild_c_cache_locked(); +} + +} // namespace + +extern "C" { + +const cfc_layout_t* cfc_layout_find(const char* name) +{ + if (!name) return nullptr; + std::lock_guard lk(g_mu); + ensure_loaded_locked(); + for (const auto& l : g_c_cache) { + if (std::strcmp(l.name, name) == 0) return &l; + } + return nullptr; +} + +const cfc_layout_t* cfc_layout_all(int* out_count) +{ + std::lock_guard lk(g_mu); + ensure_loaded_locked(); + if (out_count) *out_count = static_cast(g_c_cache.size()); + return g_c_cache.data(); +} + +void cfc_layout_to_pixels(const cfc_cell_t* cell, int W, int H, + int* out_x, int* out_y, int* out_w, int* out_h) +{ + if (!cell) return; + int x = (cell->col * W) / CFC_GRID_COLS; + int y = (cell->row * H) / CFC_GRID_ROWS; + int w = (cell->cs * W) / CFC_GRID_COLS; + int h = (cell->rs * H) / CFC_GRID_ROWS; + x &= ~1; y &= ~1; w &= ~1; h &= ~1; + if (x + w > W) w = W - x; + if (y + h > H) h = H - y; + if (out_x) *out_x = x; + if (out_y) *out_y = y; + if (out_w) *out_w = w; + if (out_h) *out_h = h; +} + +int cfc_layout_load_file(const char* path) +{ + if (!path) return -3; + int r = cfc::load_into_current(path); /* обновит global registry */ + if (r > 0) { + std::lock_guard lk(g_mu); + g_cpp_cache = cfc::current_templates(); + rebuild_c_cache_locked(); + g_loaded_path = path; + } + return r; +} + +int cfc_layout_reload(void) +{ + std::string path; + { + std::lock_guard lk(g_mu); + path = g_loaded_path; + } + if (path.empty()) return -1; + return cfc_layout_load_file(path.c_str()); +} + +const char* cfc_layout_loaded_path(void) +{ + std::lock_guard lk(g_mu); + return g_loaded_path.empty() ? nullptr : g_loaded_path.c_str(); +} + +} // extern "C" diff --git a/src/cpp/template_loader.cpp b/src/cpp/template_loader.cpp index fb6b285..34ece79 100644 --- a/src/cpp/template_loader.cpp +++ b/src/cpp/template_loader.cpp @@ -6,6 +6,7 @@ #include #include +#include #include namespace cfc { @@ -133,4 +134,39 @@ std::vector builtin_templates() return v; } +/* ── Global registry ─────────────────────────────────────────────────── */ +namespace { +std::mutex g_reg_mu; +std::vector g_registry; + +void ensure_registry_locked() +{ + if (g_registry.empty()) g_registry = builtin_templates(); +} +} // namespace + +const std::vector& current_templates() +{ + std::lock_guard lk(g_reg_mu); + ensure_registry_locked(); + return g_registry; +} + +void set_current_templates(std::vector new_templates) +{ + if (new_templates.empty()) return; + std::lock_guard lk(g_reg_mu); + g_registry = std::move(new_templates); +} + +int load_into_current(const std::string& path) +{ + std::vector v; + int r = load_templates_from_file(path, v); + if (r > 0) { + set_current_templates(std::move(v)); + } + return r; +} + } // namespace cfc diff --git a/src/layouts.c b/src/layouts.c deleted file mode 100644 index 9b364c2..0000000 --- a/src/layouts.c +++ /dev/null @@ -1,275 +0,0 @@ -/* Layout-templates на 8×8 микро-сетке (Phase 11). - * - * Step 3: JSON-based templates + hot-reload через ZMQ. Built-in templates - * остаются как fallback (если JSON-файл недоступен). - * - * Лицензия: LGPL-2.1+ - */ - -#include "../include/cuframes_composer/layouts.h" - -#include -#include -#include -#include -#include -#include -#include - -static cfc_layout_t g_layouts[64]; -static int g_layouts_count = 0; -static char g_loaded_path[512] = {0}; -static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER; - -static void recount_camera_cells(cfc_layout_t *l) -{ - int n = 0; - for (int i = 0; i < l->nb_cells; i++) { - if (l->cells[i].role == CFC_CELL_CAMERA) n++; - } - l->nb_camera_cells = n; -} - -static cfc_layout_t *push_layout(const char *name, int priority) -{ - if (g_layouts_count >= (int)(sizeof(g_layouts) / sizeof(g_layouts[0]))) return NULL; - cfc_layout_t *l = &g_layouts[g_layouts_count++]; - memset(l, 0, sizeof(*l)); - strncpy(l->name, name, sizeof(l->name) - 1); - l->name[sizeof(l->name) - 1] = '\0'; - l->priority = priority; - return l; -} - -static void push_cell(cfc_layout_t *l, int col, int row, int cs, int rs, - cfc_cell_role_t role, int order, const char *widget) -{ - if (l->nb_cells >= CFC_LAYOUT_MAX_CELLS) return; - cfc_cell_t *c = &l->cells[l->nb_cells++]; - c->col = col; c->row = row; - c->cs = cs; c->rs = rs; - c->role = role; - c->order = order; - if (widget) { - strncpy(c->widget, widget, sizeof(c->widget) - 1); - c->widget[sizeof(c->widget) - 1] = '\0'; - } else { - c->widget[0] = '\0'; - } -} - -/* Built-in fallback templates — используются если JSON не загружен. */ -static void load_builtin_layouts_locked(void) -{ - g_layouts_count = 0; - - cfc_layout_t *l; - - /* tpl_1 — одна камера во весь экран. */ - l = push_layout("tpl_1", 0); - push_cell(l, 0, 0, 8, 8, CFC_CELL_CAMERA, 0, NULL); - recount_camera_cells(l); - - /* tpl_4 — quad 2×2 (cells 4×4 микроячейки, 960×540 16:9). */ - l = push_layout("tpl_4", 0); - push_cell(l, 0, 0, 4, 4, CFC_CELL_CAMERA, 0, NULL); - push_cell(l, 4, 0, 4, 4, CFC_CELL_CAMERA, 1, NULL); - push_cell(l, 0, 4, 4, 4, CFC_CELL_CAMERA, 2, NULL); - push_cell(l, 4, 4, 4, 4, CFC_CELL_CAMERA, 3, NULL); - recount_camera_cells(l); -} - -static void ensure_loaded(void) -{ - if (g_layouts_count > 0) return; - load_builtin_layouts_locked(); -} - -const cfc_layout_t *cfc_layout_find(const char *name) -{ - if (!name) return NULL; - pthread_mutex_lock(&g_mu); - ensure_loaded(); - const cfc_layout_t *found = NULL; - for (int i = 0; i < g_layouts_count; i++) { - if (!strcmp(g_layouts[i].name, name)) { found = &g_layouts[i]; break; } - } - pthread_mutex_unlock(&g_mu); - return found; -} - -const cfc_layout_t *cfc_layout_all(int *out_count) -{ - pthread_mutex_lock(&g_mu); - ensure_loaded(); - if (out_count) *out_count = g_layouts_count; - const cfc_layout_t *p = g_layouts; - pthread_mutex_unlock(&g_mu); - return p; -} - -void cfc_layout_to_pixels(const cfc_cell_t *cell, int W, int H, - int *out_x, int *out_y, int *out_w, int *out_h) -{ - if (!cell) return; - int x = (cell->col * W) / CFC_GRID_COLS; - int y = (cell->row * H) / CFC_GRID_ROWS; - int w = (cell->cs * W) / CFC_GRID_COLS; - int h = (cell->rs * H) / CFC_GRID_ROWS; - x &= ~1; y &= ~1; w &= ~1; h &= ~1; - if (x + w > W) w = W - x; - if (y + h > H) h = H - y; - if (out_x) *out_x = x; - if (out_y) *out_y = y; - if (out_w) *out_w = w; - if (out_h) *out_h = h; -} - -/* ── JSON loader ──────────────────────────────────────────────────────── */ - -static int parse_role(const char *s) -{ - if (!s) return CFC_CELL_CAMERA; - if (!strcmp(s, "widget")) return CFC_CELL_WIDGET; - return CFC_CELL_CAMERA; -} - -static int json_int(struct json_object *obj, const char *key, int def) -{ - struct json_object *v; - if (!json_object_object_get_ex(obj, key, &v)) return def; - return json_object_get_int(v); -} - -static const char *json_str(struct json_object *obj, const char *key) -{ - struct json_object *v; - if (!json_object_object_get_ex(obj, key, &v)) return NULL; - return json_object_get_string(v); -} - -static int parse_template(struct json_object *jt, cfc_layout_t *out) -{ - memset(out, 0, sizeof(*out)); - const char *name = json_str(jt, "name"); - if (!name) { - fprintf(stderr, "[cfc/layouts] template без 'name'\n"); - return -1; - } - strncpy(out->name, name, sizeof(out->name) - 1); - out->priority = json_int(jt, "priority", 0); - - struct json_object *jcells; - if (!json_object_object_get_ex(jt, "cells", &jcells) || - !json_object_is_type(jcells, json_type_array)) { - fprintf(stderr, "[cfc/layouts] template '%s' без 'cells'\n", name); - return -1; - } - - int nb = (int)json_object_array_length(jcells); - if (nb > CFC_LAYOUT_MAX_CELLS) { - fprintf(stderr, "[cfc/layouts] template '%s' has %d cells > max %d, truncated\n", - name, nb, CFC_LAYOUT_MAX_CELLS); - nb = CFC_LAYOUT_MAX_CELLS; - } - - for (int i = 0; i < nb; i++) { - struct json_object *jc = json_object_array_get_idx(jcells, i); - if (!jc) continue; - cfc_cell_t *c = &out->cells[out->nb_cells]; - c->col = json_int(jc, "col", 0); - c->row = json_int(jc, "row", 0); - c->cs = json_int(jc, "cs", 1); - c->rs = json_int(jc, "rs", 1); - c->role = parse_role(json_str(jc, "role")); - c->order = json_int(jc, "order", 0); - const char *w = json_str(jc, "widget"); - if (w) { - strncpy(c->widget, w, sizeof(c->widget) - 1); - c->widget[sizeof(c->widget) - 1] = '\0'; - } - /* Валидация bounds. */ - if (c->cs < 1 || c->rs < 1 || - c->col < 0 || c->row < 0 || - c->col + c->cs > CFC_GRID_COLS || - c->row + c->rs > CFC_GRID_ROWS) { - fprintf(stderr, "[cfc/layouts] '%s' cell[%d] outside grid: col=%d row=%d cs=%d rs=%d — пропуск\n", - name, i, c->col, c->row, c->cs, c->rs); - continue; - } - out->nb_cells++; - } - recount_camera_cells(out); - return 0; -} - -int cfc_layout_load_file(const char *path) -{ - if (!path) return -3; - FILE *f = fopen(path, "r"); - if (!f) { - fprintf(stderr, "[cfc/layouts] %s: open failed: %s\n", path, strerror(errno)); - return -3; - } - fseek(f, 0, SEEK_END); - long sz = ftell(f); - fseek(f, 0, SEEK_SET); - if (sz <= 0 || sz > 1 << 20) { fclose(f); return -1; } - char *buf = malloc(sz + 1); - if (!buf) { fclose(f); return -1; } - if ((long)fread(buf, 1, sz, f) != sz) { free(buf); fclose(f); return -1; } - buf[sz] = '\0'; - fclose(f); - - struct json_object *root = json_tokener_parse(buf); - free(buf); - if (!root) { - fprintf(stderr, "[cfc/layouts] %s: JSON parse failed\n", path); - return -1; - } - - struct json_object *jtpls; - if (!json_object_object_get_ex(root, "templates", &jtpls) || - !json_object_is_type(jtpls, json_type_array)) { - fprintf(stderr, "[cfc/layouts] %s: 'templates' array missing\n", path); - json_object_put(root); - return -2; - } - int n = (int)json_object_array_length(jtpls); - if (n <= 0) { - fprintf(stderr, "[cfc/layouts] %s: 'templates' пуст\n", path); - json_object_put(root); - return -2; - } - - pthread_mutex_lock(&g_mu); - int new_count = 0; - cfc_layout_t tmp[64]; - for (int i = 0; i < n && new_count < (int)(sizeof(tmp)/sizeof(tmp[0])); i++) { - struct json_object *jt = json_object_array_get_idx(jtpls, i); - if (parse_template(jt, &tmp[new_count]) == 0) new_count++; - } - if (new_count > 0) { - memcpy(g_layouts, tmp, sizeof(cfc_layout_t) * new_count); - g_layouts_count = new_count; - strncpy(g_loaded_path, path, sizeof(g_loaded_path) - 1); - g_loaded_path[sizeof(g_loaded_path) - 1] = '\0'; - fprintf(stderr, "[cfc/layouts] %s: loaded %d templates\n", path, new_count); - } else { - fprintf(stderr, "[cfc/layouts] %s: no valid templates, keeping current\n", path); - } - pthread_mutex_unlock(&g_mu); - json_object_put(root); - return new_count; -} - -int cfc_layout_reload(void) -{ - if (!g_loaded_path[0]) return -1; - return cfc_layout_load_file(g_loaded_path); -} - -const char *cfc_layout_loaded_path(void) -{ - return g_loaded_path[0] ? g_loaded_path : NULL; -}