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

23 KiB
Raw Blame History

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.

  1. Создать 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
  1. Реализация 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
  1. Добавить в src/CMakeLists.txt:
set(COMPOSER_SOURCES_CPP
    ...
    cpp/graph_cell.cpp
)
  1. Использовать в 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));
}
  1. Подключить 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»).

  1. 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_;
};
  1. Реализация:
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);
}
  1. Подключать в 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 на 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.hppComposerConfig::shrink_hysteresis_ms
Поменять цвет border'а cells layout.cppborder_style.color_y/u/v
Поменять label-font mqtt_overlay.cppLabelDecoration style + LabelStyle::font_path
Добавить ZMQ-verb control.c::dispatch() + новая cmd_* функция
Поменять manual override длительность composer.hppmanual_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 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::applymotion_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 с описанием ключевых изменений.