Files
cuframes-composer/docs/ru/developer.md
T
gx e76360dbc4 docs: руководства пользователя / разработчика / operations (RU+EN)
Полный комплект документации к 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>
2026-06-04 22:02:47 +01:00

616 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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. Жизненный цикл одного кадра
```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<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):
```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 <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
```
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<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
```
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<GraphCell>(r, /*max_points=*/120));
} else {
cells_.push_back(std::make_unique<WidgetCell>(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<BadgeDecoration>(bs));
}
```
## 6. SourcePool и motion-state
```cpp
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
```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<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.
```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<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:
```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<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
```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<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 на 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<CameraCell*>` в `Layout::find_camera_cell_rect`** —
использует RTTI. Включён `-frtti` по умолчанию в g++/nvcc.
## 14. Workflow по изменению Phase-задач
1. Branch `feature/<phase>-<feature>` от `main`
2. Реализация, host build PASS, jammy build PASS (см. operations.md)
3. Bake image `gx/cuframes-composer:<phase>-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 с описанием
ключевых изменений.