Полный комплект документации к Phase 11b:
docs/ru/user.md — для админа инсталляции (motion-mode, PTZ,
templates.json, mqtt_overlays.json, ZMQ verbs)
docs/ru/developer.md — архитектура (Cell / Layout / Decoration),
как добавить новый Cell/Decoration, ABI shim,
algorithms (best-fit + asymmetric hysteresis)
docs/ru/operations.md — build (host + jammy + incremental bake),
deploy, logs/telemetry, troubleshooting
(broken pipe, MQTT-overlay, motion-mode)
docs/en/*.md — английская версия всех трёх
README.md — переписан с overview + ссылками на docs/
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
23 KiB
cfc-grid — руководство разработчика
Аудитория: разработчик который правит C++ код композитора, добавляет новые типы Cell/Decoration/overlay-type, или меняет логику auto-layout.
Если ты пользователь — см. user.md. Если занимаешься deploy/troubleshooting — см. operations.md.
1. Архитектура (40000-foot view)
┌──────────────────────────────────────────────┐
│ cfc::Composer (C++) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
│ │SourcePool│ │ Layout │ │OutputSurface │ │
│ │ (pool of │ │ vector< │ │ CudaBuffer │ │
│ │ cfc_ │ │ unique_ │ │ (VMM NV12) │ │
│ │source_t*)│ │ ptr<Cell>│ │ │ │
│ └──────────┘ └──────────┘ └──────────────┘ │
│ ↓ ↓ ↑ │
│ motion_pulse apply() NVENC │
└──────────────────────────────────────────────┘
↑ ↓
extern "C" ABI shim H.264 pipe
(composer_c_api.cpp, ↓
layouts_c_api.cpp, ffmpeg-relay
mqtt_overlay_c_api.cpp) ↓
↑ mediamtx
┌────────┴────────┐ ↓
│ │ ↓
grid_record.c control.c clients
(C, CLI parsing) (ZMQ verbs)
↑ ↑
│ │
frigate_mqtt.c health.c, audio.c, writer.c, source.c
(Frigate events) (C-only modules)
Принципы:
- C++17 host-side, CUDA kernels остались на C
- Zero-copy: output VMM-буфер передаётся как
NV12Refвсем cells/decorations (read-write reference, не копия) - Coexistence: C-модули (
source.c,nvenc.c,frigate_mqtt.c,health.c,writer.c,audio.c,overlay.c) сохранены, доступ черезextern "C". Толькоcomposer.cиlayouts.cудалены (заменены C++ + ABI shim). grid_record.c(CLI entry-point) остался на C, использует публичныйcfc_composer_*API без правок
2. Дерево исходников
include/cuframes_composer/
├── *.h # Публичный C API (extern "C")
│ ├── composer.h # cfc_composer_* ABI
│ ├── layouts.h # cfc_layout_* ABI
│ ├── overlay.h # cfc_overlay_* (text/png/border/detbox)
│ ├── source.h, nvenc.h, frigate_mqtt.h, ...
│ └── ...
└── cpp/ # C++ headers — публичные классы
├── cuda_raii.hpp # CudaBuffer, CudaStream (RAII)
├── types.hpp # Rect, NV12Ref
├── cell.hpp # абстрактный Cell + draw()
├── camera_cell.hpp # CameraCell : Cell
├── widget_cell.hpp # WidgetCell : Cell
├── blank_cell.hpp # BlankCell : Cell
├── decoration.hpp # абстрактный Decoration
├── border_decoration.hpp # BorderDecoration
├── label_decoration.hpp # LabelDecoration (FreeType internal)
├── template.hpp # LayoutTemplate, CellTemplate, kGridCols/Rows
├── template_loader.hpp # JSON parser, current_templates()
├── source_pool.hpp # SourcePool, PoolEntry
├── layout.hpp # Layout
├── composer.hpp # Composer (главный класс)
└── mqtt_overlay.hpp # MqttOverlayItem + Manager
src/
├── *.c # C-модули (source/nvenc/health/...)
├── overlay.c # Все типы cfc_overlay_t
├── frigate_mqtt.c # Subscribe + motion_pulse + zone-filter
├── cugrid/cugrid.cu # CUDA kernels (resize, fill, blit)
└── cpp/ # C++ реализации
├── camera_cell.cpp, widget_cell.cpp, blank_cell.cpp
├── border_decoration.cpp, label_decoration.cpp
├── source_pool.cpp
├── template_loader.cpp
├── layout.cpp
├── composer.cpp
├── composer_c_api.cpp # extern "C" shim для composer
├── layouts_c_api.cpp # extern "C" shim для layouts
├── mqtt_overlay.cpp
└── mqtt_overlay_c_api.cpp
examples/
├── simple_record.c # 1-источник → H.264 в файл
├── grid_record.c # Main CLI entry-point (C)
└── grid_record_cpp.cpp # C++ smoke (использует cfc::Composer напрямую)
3. Жизненный цикл одного кадра
// В compose loop (grid_record.c, через cfc_composer_compose):
NV12Ref ref = composer.compose_frame();
// 1. maybe_relayout(): motion-mode? best-fit template; apply Layout
// 2. compose_clear(): fill output буфера BT.709 black
// 3. Layout::render(): for each cell: draw_content() + decorations[]
// 4. for each overlay: sync detbox geom + cfc_overlay_draw()
// cudaStreamSynchronize(0);
// nvenc.encode(ref.y_ptr, ref.pitch_y, ref.uv_ptr, ref.pitch_uv, ...);
Все операции выполняются на CUDA default stream. Zero-copy:
ref.y_ptr / ref.uv_ptr — те же CUdeviceptr, что внутри
composer.output_ (RAII CudaBuffer).
4. Cell — иерархия и расширение
4.1 Абстракция
class Cell {
public:
explicit Cell(const Rect& geom);
virtual ~Cell() = default;
Cell(const Cell&) = delete;
const Rect& geometry() const noexcept;
void set_geometry(const Rect& r) noexcept;
void add_decoration(std::unique_ptr<Decoration>);
void draw(CUstream stream, NV12Ref& dst); // public — calls draw_content() + decorations
protected:
virtual void draw_content(CUstream stream, NV12Ref& dst) = 0;
Rect geom_;
std::vector<std::unique_ptr<Decoration>> decorations_;
};
Cell::draw() финальный (не виртуальный — final pattern через NVI):
void Cell::draw(CUstream stream, NV12Ref& dst) {
if (geom_.empty()) return;
draw_content(stream, dst); // subclass renders content
for (auto& d : decorations_) {
d->draw(stream, dst, geom_); // overlay decorations on top
}
}
4.2 Существующие подклассы
| Класс | draw_content реализация |
|---|---|
CameraCell |
cfc_source_get_latest() + cfc_cugrid_resize_nv12 |
WidgetCell |
cfc_cugrid_fill_nv12 тёмно-серым Y=40 (placeholder) |
BlankCell |
cfc_cugrid_fill_nv12 BT.709 черным Y=16 |
4.3 Как добавить новый тип Cell
Пример: GraphCell — рисует scrolling-chart из подписки на MQTT.
- Создать
include/cuframes_composer/cpp/graph_cell.hpp:
#ifndef CUFRAMES_COMPOSER_CPP_GRAPH_CELL_HPP
#define CUFRAMES_COMPOSER_CPP_GRAPH_CELL_HPP
#include "cell.hpp"
#include <deque>
namespace cfc {
class GraphCell : public Cell {
public:
GraphCell(const Rect& geom, std::size_t max_points = 60)
: Cell(geom), max_points_(max_points) {}
void push_value(double v) {
if (values_.size() >= max_points_) values_.pop_front();
values_.push_back(v);
}
protected:
void draw_content(CUstream stream, NV12Ref& dst) override;
private:
std::deque<double> values_;
std::size_t max_points_;
};
} // namespace cfc
#endif
- Реализация
src/cpp/graph_cell.cpp:
#include "../../include/cuframes_composer/cpp/graph_cell.hpp"
#include "../../include/cuframes_composer/cugrid.h"
namespace cfc {
void GraphCell::draw_content(CUstream stream, NV12Ref& dst) {
if (geom_.empty() || values_.empty()) return;
// 1. BG fill
cfc_cugrid_fill_nv12(stream, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
geom_.x, geom_.y, geom_.w, geom_.h,
/*Y*/ 30, 128, 128, 255);
// 2. Compute min/max
double mn = *std::min_element(values_.begin(), values_.end());
double mx = *std::max_element(values_.begin(), values_.end());
if (mx == mn) mx = mn + 1.0;
// 3. Render line as thin filled rects (lazy, без kernel'я для bitmap'а)
int n = static_cast<int>(values_.size());
int dx = geom_.w / n;
for (int i = 0; i < n; i++) {
double norm = (values_[i] - mn) / (mx - mn);
int y_px = geom_.y + geom_.h - 2 - (int)(norm * (geom_.h - 4));
cfc_cugrid_fill_nv12(stream, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
geom_.x + i * dx, y_px, dx & ~1, 2,
/*Y*/ 235, 128, 64, 255); // limited-range bright
}
}
} // namespace cfc
- Добавить в
src/CMakeLists.txt:
set(COMPOSER_SOURCES_CPP
...
cpp/graph_cell.cpp
)
- Использовать в
Layout::apply():
// В layout.cpp, в обработке widget cells:
if (wt->widget == "graph_temp") {
cells_.push_back(std::make_unique<GraphCell>(r, /*max_points=*/120));
} else {
cells_.push_back(std::make_unique<WidgetCell>(r, wt->widget));
}
- Подключить MQTT →
GraphCell::push_value()— либо через MqttOverlayManager расширение, либо черезcfc::Composer::register_graph_feed()(новый API).
5. Decoration — композиция в Cell
5.1 Абстракция
class Decoration {
public:
virtual ~Decoration() = default;
virtual void draw(CUstream stream, NV12Ref& dst, const Rect& parent_rect) = 0;
};
Decoration знает только pixel-rect parent-cell'а — позиционируется относительно него. Не имеет access к Layout/Composer.
5.2 Существующие подклассы
LabelDecoration— FreeType atlas +cfc_cugrid_blit_rgba_nv12. Persistent VRAM (rebuild приset_text). Поддерживает background fill (черезcfc_overlay_text_config_t.bg_*параметры).BorderDecoration— 4 вызоваcfc_cugrid_fill_nv12(top, bottom, left, right rect'ы поthickness).
5.3 Как добавить новый тип Decoration
Пример: BadgeDecoration — иконка в углу (например красная точка
«recording»).
- Header
badge_decoration.hpp:
class BadgeDecoration : public Decoration {
public:
struct Style {
int corner = 0; // 0=top-left, 1=top-right, 2=bottom-right, 3=bottom-left
int size = 12;
int margin = 8;
int color_y = 80, color_u = 90, color_v = 240; // red-ish
int alpha = 240;
bool visible = true;
};
explicit BadgeDecoration(const Style& s) : style_(s) {}
void set_visible(bool v) noexcept { style_.visible = v; }
void draw(CUstream stream, NV12Ref& dst, const Rect& p) override;
private:
Style style_;
};
- Реализация:
void BadgeDecoration::draw(CUstream s, NV12Ref& dst, const Rect& p) {
if (!style_.visible || style_.alpha <= 0) return;
int x, y;
switch (style_.corner) {
case 0: x = p.x + style_.margin; y = p.y + style_.margin; break;
case 1: x = p.x + p.w - style_.size - style_.margin; y = p.y + style_.margin; break;
case 2: x = p.x + p.w - style_.size - style_.margin; y = p.y + p.h - style_.size - style_.margin; break;
case 3: x = p.x + style_.margin; y = p.y + p.h - style_.size - style_.margin; break;
}
x &= ~1; y &= ~1;
cfc_cugrid_fill_nv12(s, dst.y_ptr, dst.pitch_y, dst.uv_ptr, dst.pitch_uv,
x, y, style_.size, style_.size,
style_.color_y, style_.color_u, style_.color_v, style_.alpha);
}
- Подключать в
Layout::apply():
if (cc->source()->state() == CFC_SOURCE_STALE) {
BadgeDecoration::Style bs;
bs.corner = 1; bs.color_y = 180; bs.color_v = 100; // yellow
cell->add_decoration(std::make_unique<BadgeDecoration>(bs));
}
6. SourcePool и motion-state
struct PoolEntry {
std::string cuframes_key;
std::string frigate_camera;
int priority;
cfc_source_t* source;
std::atomic<int64_t> last_motion_ms;
std::vector<std::string> required_zones;
cfc_source_state_t state() const; // вызывает cfc_source_get_latest
bool drawable() const; // ACTIVE || STALE
};
class SourcePool {
public:
int add(key, frigate_camera, priority, zones, SubscribeOpts);
PoolEntry* by_key(const std::string&);
PoolEntry* by_frigate_camera(const std::string&);
template<typename F> void for_each(F&&);
void motion_pulse(const std::string& frigate_camera,
const std::vector<std::string>& current_zones);
};
motion_pulse вызывается из frigate_mqtt.c при каждом event. Если
required_zones непустой — match по intersection с event.current_zones,
иначе принимаем всё.
7. Auto-layout алгоритмы
См. cfc::Composer::maybe_relayout() (src/cpp/composer.cpp).
7.1 Best-fit selection
const LayoutTemplate* Composer::pick_best_fit(int need) const {
const auto& reg = current_templates();
const LayoutTemplate* best = nullptr;
int best_waste = -1, best_prio = -1;
for (auto& t : reg) {
int n = t.nb_camera_cells();
if (n < need) continue;
int waste = n - need;
if (!best || waste < best_waste ||
(waste == best_waste && t.priority > best_prio)) {
best = &t; best_waste = waste; best_prio = t.priority;
}
}
if (best) return best;
// overflow → largest
for (auto& t : reg) {
if (!best || t.nb_camera_cells() > best->nb_camera_cells()) best = &t;
}
return best;
}
7.2 Collect active
std::vector<PoolEntry*> Composer::collect_active() const {
std::vector<PoolEntry*> active;
int64_t now = now_ms_mono();
pool_.for_each([&](PoolEntry& e) {
if (!e.drawable()) return; // DEAD/CONNECTING — skip
int64_t last = e.last_motion_ms.load();
if (last == 0) return; // never had motion
if (now - last > cfg_.motion_ttl_ms) return; // TTL expired
active.push_back(&e);
});
// idle fallback: top-priority drawable
if (active.empty()) {
PoolEntry* best = nullptr;
pool_.for_each([&](PoolEntry& e) {
if (!e.drawable()) return;
if (!best || e.priority > best->priority) best = &e;
});
if (best) active.push_back(best);
}
std::sort(active.begin(), active.end(),
[](PoolEntry* a, PoolEntry* b) { return a->priority > b->priority; });
return active;
}
7.3 Asymmetric hysteresis
Сигнатура template_name + "|" + sorted_keys запоминается. Если новый
набор ⊇ committed (рост) → commit мгновенно. Иначе ждём
shrink_hysteresis_ms (default 3000) → commit.
bool is_grow = std::includes(nkeys.begin(), nkeys.end(),
ckeys.begin(), ckeys.end());
if (is_grow && nkeys.size() == ckeys.size()) is_grow = false;
if (is_grow) { /* commit */ } else {
if (sig != pending_signature_) {
pending_signature_ = sig;
pending_first_seen_ms_ = now;
}
if (now - pending_first_seen_ms_ < cfg_.shrink_hysteresis_ms) return;
/* commit */
}
7.4 Fill свободных cells
После выбора template и cap'а по nb_camera_cells:
if (active.size() < cap) {
std::vector<PoolEntry*> extras;
pool_.for_each([&](PoolEntry& e) {
if (!e.drawable()) return;
if (e ∈ already_active) return;
extras.push_back(&e);
});
std::sort(extras.begin(), extras.end(), priority_desc);
while (active.size() < cap && !extras.empty()) {
active.push_back(extras.front());
extras.erase(extras.begin());
}
}
7.5 Manual PTZ override
Composer::set_layout(name) в motion-mode:
manual_override_until_ms_ = now + manual_override_duration_ms_; // default 60s
maybe_relayout() пропускает работу пока now < manual_override_until_ms_.
По истечении — committed_signature_.clear() → форс relayout.
8. extern "C" ABI shim
composer_c_api.cpp — тонкая обёртка:
extern "C" int cfc_composer_create(const cfc_composer_config_t* cfg,
cfc_composer_t** out)
{
cfc::ComposerConfig cpp_cfg = {/* ... */};
auto* comp = new (std::nothrow) cfc::Composer(cpp_cfg);
if (!comp->ok()) { delete comp; return -1; }
// manual_cells из cfg->cells → set_manual_cells()
*out = reinterpret_cast<cfc_composer_t*>(comp);
return 0;
}
cfc_composer_t — opaque в C, reinterpret_cast к/от cfc::Composer*
в shim'е.
layouts_c_api.cpp — аналогично для cfc_layout_*. Держит static
кеш vector<cfc_layout_t> который пересинхронизируется с
cfc::current_templates() при reload.
9. Build
9.1 CMake
project(cuframes-composer LANGUAGES C CXX CUDA)
set(CMAKE_CXX_STANDARD 17)
COMPOSER_SOURCES_CPP в src/CMakeLists.txt перечисляет все .cpp.
9.2 Host build (для CI / dev-машины Ubuntu 24.04)
cd /home/claude/projects/cuframes-composer
mkdir -p build && cd build
cmake -DCMAKE_BUILD_TYPE=Release ..
make -j$(nproc)
9.3 Production build (Ubuntu 22.04 jammy)
См. operations.md. Кратко:
docker run --rm --gpus all \
-v "$PWD":/src -w /src/build-jammy \
cuframes-composer-builder:cached \
bash -c 'cmake .. && make -j$(nproc)'
10. Производительность
10.1 Гарантии zero-copy
- Один
CudaBuffer output_наComposer, передаётся какNV12Refвсем cells/decorations - Cell не создаёт VRAM-allocation'ов на кадр (только
Decoration::drawможет делать FreeType-rebuild приset_text— это offline) - Layout::apply() пересоздаёт
vector<unique_ptr<Cell>>только при смене template'а; обычно раз в N секунд
10.2 Virtual call overhead
Cell::draw() → draw_content() virtual call: 1 indirect call per cell
per frame. При 25 fps × 16 cells = 400 calls/sec — нерелевантно.
10.3 Hot path
CUDA kernels (cugrid.cu):
fill_nv12— 1 kernel launch на rectresize_nv12— bilinear через 2 kernels (Y plane + UV plane)blit_rgba_nv12— 1 kernel, RGBA → NV12 + alpha-blend
Все Cell-операции конвертируются в N×fill_nv12 + 1×resize_nv12 + M×blit_rgba_nv12 — оптимизировать через batching пока не нужно (GPU не sticky на текущей нагрузке).
11. Где править распространённые задачи
| Хочу… | Файл |
|---|---|
| Изменить hysteresis | composer.hpp → ComposerConfig::shrink_hysteresis_ms |
| Поменять цвет border'а cells | layout.cpp → border_style.color_y/u/v |
| Поменять label-font | mqtt_overlay.cpp → LabelDecoration style + LabelStyle::font_path |
| Добавить ZMQ-verb | control.c::dispatch() + новая cmd_* функция |
| Поменять manual override длительность | composer.hpp → manual_override_duration_ms_ |
| Добавить новый MQTT-overlay anchor | mqtt_overlay.cpp::reposition_overlay() switch |
| Поддержать color emoji | overlay.c::text_rebuild_atlas() — handle FT_LOAD_COLOR + bitmap BGRA → blit как RGBA |
12. Тестирование
12.1 Юнит (не реализован)
Запланировано: catch2-тесты для pick_best_fit, collect_active,
hysteresis. Зависимость на CUDA — mock через cfc_source_get_latest
шим.
12.2 Integration smoke (examples/grid_record_cpp.cpp)
Минимальный C++ smoke: init Composer, compose loop, dump NV12 → файл. Не использует NVENC (тестирует только композицию).
build/examples/grid_record_cpp \
--out=/tmp/dump.nv12 --frames=10 --width=1920 --height=1080 \
--templates=docker/templates.json \
--source=cam-parking,frigate=parking_overview,priority=100 \
--motion-mode
12.3 Production smoke
docker logs cfc-grid | grep -E "loaded|template|motion|grow|shrink" —
живая телеметрия композитора.
13. Известные подводные камни
cfc_overlay_tне RAII — managed черезcfc_composer_add_overlay/cfc_overlay_destroy(Composer.cpp::~Composer уничтожает все).pthread_mutex_tв SourcePool vsstd::mutex— выборstd::mutexдля C++ слоя,pthread_mutex_tдля C-слоя (PoolEntry::last_motion_ms используетstd::atomic).- Compose loop НЕ blocking на CUDA-операциях —
cudaStreamSynchronizeвызывается caller'ом (grid_record.c) перед NVENC. - Frigate event может прийти ДО Layout::apply —
motion_pulseобновляетlast_motion_ms, но cell для этой камеры ещё может не существовать. На следующем кадреmaybe_relayoutпересчитает. dynamic_cast<CameraCell*>вLayout::find_camera_cell_rect— использует RTTI. Включён-frttiпо умолчанию в g++/nvcc.
14. Workflow по изменению Phase-задач
- Branch
feature/<phase>-<feature>отmain - Реализация, host build PASS, jammy build PASS (см. operations.md)
- Bake image
gx/cuframes-composer:<phase>-stepN - Deploy на dev-target → smoke verify через VLC/logs
- Commit + push branch
- (Если нужно) — merge в
mainчерез--no-ffPR-style
Для multi-commit фазы: один WIP-merge commit на main с описанием ключевых изменений.