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:
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user