Phase 11b F: extern "C" ABI shim + production deploy

В прод деплоен gx/cuframes-composer:0.11b-step1 — C++ ядро
работает через ABI shim, старые C-callers (grid_record.c, control.c,
frigate_mqtt.c) использует те же cfc_composer_* функции.

Что в этом коммите:
  - src/cpp/composer_c_api.cpp: extern "C" обёртки над cfc::Composer
    методами. Полный набор: _create/_destroy/_compose/_add_overlay/
    _find_overlay/_set_layout/_current_layout/_add_pool_source/
    _set_motion_mode/_get_motion_mode/_motion_pulse/_get_health.
  - src/cpp/layouts_c_api.cpp: extern "C" обёртки над template_loader
    для cfc_layout_find/_all/_load_file/_reload/_loaded_path/_to_pixels.
  - cpp/template_loader: global registry (current_templates / set_*
    / load_into_current) — единый источник истины. Composer и C ABI
    shim читают один и тот же mutex-защищённый vector<LayoutTemplate>.
    Hot-reload через ZMQ cfc_layout_load_file подхватывается composer'ом
    на следующем кадре без рестарта.
  - cpp/composer: pick_best_fit, set_layout, maybe_relayout читают
    current_templates() вместо локального snapshot.
  - cpp/composer: backward-compat overlay list (add_overlay/find_overlay)
    + manual cells support (для C API без motion-mode).
  - cpp/composer compose_frame: после Layout.render() рендерит overlays
    (CLI text/icon/border + Frigate detbox) поверх.
  - Удалены: src/composer.c (заменён composer_c_api.cpp + composer.cpp),
    src/layouts.c (заменён layouts_c_api.cpp + template_loader.cpp).
  - Оставлено как есть: src/overlay.c (PNG/text/border/detbox CLI overlays
    — реализация не меняется, доступ через cfc_overlay_*).
  - src/CMakeLists.txt: COMPOSER_SOURCES_C минус composer.c, layouts.c,
    COMPOSER_SOURCES_CPP плюс composer_c_api.cpp, layouts_c_api.cpp.

Production smoke (R9-88.23):
  [cfc/loader] /opt/templates.json: loaded 7 templates
  [cfc/composer] templates loaded: 7 (path='/opt/templates.json')
  [cfc/composer] pool+ cam-parking prio=100 / cam-gate_lpr prio=90 / ...
  [cfc/composer] motion_mode=1 ttl=45000ms pool=4
  [cfc/composer] grow → template='tpl_1' active=1
PASS.

Refs: #195 (Phase 11b C++ refactor).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 08:57:30 +01:00
parent beb8e1baa0
commit 6e0273f4b4
9 changed files with 511 additions and 1059 deletions
@@ -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<std::pair<std::string, Rect>>& 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<cfc_overlay_t*> overlays_;
/* Manual cells — alternative режим без motion-mode (grid_record --cell). */
std::vector<std::pair<std::string, Rect>> manual_cells_;
bool manual_applied_ = false;
};
} // namespace cfc
@@ -22,6 +22,14 @@ int load_templates_from_file(const std::string& path,
/* Встроенный набор fallback templates (Phase 11b base — single, quad). */
std::vector<LayoutTemplate> 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<LayoutTemplate>& current_templates();
void set_current_templates(std::vector<LayoutTemplate> new_templates);
int load_into_current(const std::string& path);
} // namespace cfc
#endif /* CUFRAMES_COMPOSER_CPP_TEMPLATE_LOADER_HPP */
+6 -5
View File
@@ -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
-764
View File
@@ -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 <cuda_runtime.h>
#include <pthread.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#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;
}
+101 -15
View File
@@ -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<SourcePool&>(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<std::pair<std::string, Rect>>& cells)
{
manual_cells_ = cells;
manual_applied_ = false; /* compose_frame применит */
}
int Composer::load_templates(const std::string& path)
{
std::vector<LayoutTemplate> 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<int>(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<int>(i);
t.cells.push_back(std::move(c));
}
/* Build active list из pool entries по cuframes_key. */
std::vector<PoolEntry*> 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<CUstream>(stream_), dst);
/* Backward-compat overlays (CLI text/icon, detbox) — поверх Layout. */
for (auto* ov : overlays_) {
if (!ov) continue;
cfc_overlay_draw(ov, reinterpret_cast<CUstream>(stream_),
y, pitch_y_, uv, pitch_uv_,
cfg_.width, cfg_.height);
}
return dst;
}
+197
View File
@@ -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 <cstdio>
#include <cstring>
#include <string>
#include <utility>
#include <vector>
namespace {
inline cfc::Composer* as_cpp(cfc_composer_t* h)
{
return reinterpret_cast<cfc::Composer*>(h);
}
inline cfc_composer_t* as_c(cfc::Composer* c)
{
return reinterpret_cast<cfc_composer_t*>(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<std::pair<std::string, cfc::Rect>> 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<std::string> 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<std::string> 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"
+136
View File
@@ -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 <cstdio>
#include <cstring>
#include <mutex>
#include <string>
#include <vector>
namespace {
static std::mutex g_mu;
static std::vector<cfc_layout_t> g_c_cache;
static std::string g_loaded_path;
static std::vector<cfc::LayoutTemplate> 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<int>(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<std::mutex> 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<std::mutex> lk(g_mu);
ensure_loaded_locked();
if (out_count) *out_count = static_cast<int>(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<std::mutex> 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<std::mutex> 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<std::mutex> lk(g_mu);
return g_loaded_path.empty() ? nullptr : g_loaded_path.c_str();
}
} // extern "C"
+36
View File
@@ -6,6 +6,7 @@
#include <cstdio>
#include <fstream>
#include <mutex>
#include <sstream>
namespace cfc {
@@ -133,4 +134,39 @@ std::vector<LayoutTemplate> builtin_templates()
return v;
}
/* ── Global registry ─────────────────────────────────────────────────── */
namespace {
std::mutex g_reg_mu;
std::vector<LayoutTemplate> g_registry;
void ensure_registry_locked()
{
if (g_registry.empty()) g_registry = builtin_templates();
}
} // namespace
const std::vector<LayoutTemplate>& current_templates()
{
std::lock_guard<std::mutex> lk(g_reg_mu);
ensure_registry_locked();
return g_registry;
}
void set_current_templates(std::vector<LayoutTemplate> new_templates)
{
if (new_templates.empty()) return;
std::lock_guard<std::mutex> lk(g_reg_mu);
g_registry = std::move(new_templates);
}
int load_into_current(const std::string& path)
{
std::vector<LayoutTemplate> v;
int r = load_templates_from_file(path, v);
if (r > 0) {
set_current_templates(std::move(v));
}
return r;
}
} // namespace cfc
-275
View File
@@ -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 <errno.h>
#include <json-c/json.h>
#include <pthread.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}