e76360dbc4
Полный комплект документации к 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>
616 lines
23 KiB
Markdown
616 lines
23 KiB
Markdown
# 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 с описанием
|
||
ключевых изменений.
|