# cfc-grid — руководство разработчика > Аудитория: разработчик который правит C++ код композитора, добавляет > новые типы Cell/Decoration/overlay-type, или меняет логику auto-layout. > > Если ты пользователь — см. [user.md](user.md). Если занимаешься > deploy/troubleshooting — см. [operations.md](operations.md). ## 1. Архитектура (40000-foot view) ``` ┌──────────────────────────────────────────────┐ │ cfc::Composer (C++) │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ │SourcePool│ │ Layout │ │OutputSurface │ │ │ │ (pool of │ │ vector< │ │ CudaBuffer │ │ │ │ cfc_ │ │ unique_ │ │ (VMM NV12) │ │ │ │source_t*)│ │ ptr│ │ │ │ │ └──────────┘ └──────────┘ └──────────────┘ │ │ ↓ ↓ ↑ │ │ 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. Жизненный цикл одного кадра ```cpp // В 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 Абстракция ```cpp 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); 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> decorations_; }; ``` `Cell::draw()` финальный (не виртуальный — final pattern через NVI): ```cpp 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. 1. Создать `include/cuframes_composer/cpp/graph_cell.hpp`: ```cpp #ifndef CUFRAMES_COMPOSER_CPP_GRAPH_CELL_HPP #define CUFRAMES_COMPOSER_CPP_GRAPH_CELL_HPP #include "cell.hpp" #include 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 values_; std::size_t max_points_; }; } // namespace cfc #endif ``` 2. Реализация `src/cpp/graph_cell.cpp`: ```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(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 ``` 3. Добавить в `src/CMakeLists.txt`: ```cmake set(COMPOSER_SOURCES_CPP ... cpp/graph_cell.cpp ) ``` 4. Использовать в `Layout::apply()`: ```cpp // В layout.cpp, в обработке widget cells: if (wt->widget == "graph_temp") { cells_.push_back(std::make_unique(r, /*max_points=*/120)); } else { cells_.push_back(std::make_unique(r, wt->widget)); } ``` 5. Подключить MQTT → `GraphCell::push_value()` — либо через MqttOverlayManager расширение, либо через `cfc::Composer::register_graph_feed()` (новый API). ## 5. Decoration — композиция в Cell ### 5.1 Абстракция ```cpp 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»). 1. Header `badge_decoration.hpp`: ```cpp 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_; }; ``` 2. Реализация: ```cpp 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); } ``` 3. Подключать в `Layout::apply()`: ```cpp 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(bs)); } ``` ## 6. SourcePool и motion-state ```cpp struct PoolEntry { std::string cuframes_key; std::string frigate_camera; int priority; cfc_source_t* source; std::atomic last_motion_ms; std::vector 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 void for_each(F&&); void motion_pulse(const std::string& frigate_camera, const std::vector& 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 ```cpp 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 ```cpp std::vector Composer::collect_active() const { std::vector 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. ```cpp 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`: ```cpp if (active.size() < cap) { std::vector 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: ```cpp 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` — тонкая обёртка: ```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(comp); return 0; } ``` `cfc_composer_t` — opaque в C, `reinterpret_cast` к/от `cfc::Composer*` в shim'е. `layouts_c_api.cpp` — аналогично для `cfc_layout_*`. Держит static кеш `vector` который пересинхронизируется с `cfc::current_templates()` при reload. ## 9. Build ### 9.1 CMake ```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) ```bash 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](operations.md). Кратко: ```bash 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>` только при смене 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 на rect - `resize_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 (тестирует только композицию). ```bash 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** vs `std::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` в `Layout::find_camera_cell_rect`** — использует RTTI. Включён `-frtti` по умолчанию в g++/nvcc. ## 14. Workflow по изменению Phase-задач 1. Branch `feature/-` от `main` 2. Реализация, host build PASS, jammy build PASS (см. operations.md) 3. Bake image `gx/cuframes-composer:-stepN` 4. Deploy на dev-target → smoke verify через VLC/logs 5. Commit + push branch 6. (Если нужно) — merge в `main` через `--no-ff` PR-style Для multi-commit фазы: один WIP-merge commit на main с описанием ключевых изменений.