Files
gx beb8e1baa0 Phase 11b B-E: ООП-гипотеза проверена end-to-end
Что в этом коммите:

Decoration реализации:
  - cpp/label_decoration.hpp/.cpp — FreeType atlas + cugrid_blit_rgba_nv12.
    UTF-8 декодер, atlas в VRAM (RAII через CudaBuffer), rebuild при set_text.
  - cpp/border_decoration.hpp/.cpp — 4 cugrid_fill_nv12 (top/bottom/left/right).

Cell реализации:
  - cpp/camera_cell.hpp/.cpp — cfc_source_get_latest + cugrid_resize_nv12.
    Non-owning указатель на cfc_source_t (pool владеет).
  - cpp/widget_cell.hpp/.cpp — тёмный fill placeholder.
  - cpp/blank_cell.hpp/.cpp — BT.709 black fill.

Layout и Template:
  - cpp/template.hpp — LayoutTemplate { name, cells[], priority }.
    8×8 микро-сетка (kGridCols=kGridRows=8). to_pixels() переводит в Rect.
  - cpp/layout.hpp/.cpp — vector<unique_ptr<Cell>>, apply() создаёт
    CameraCell/WidgetCell/BlankCell + Decorations (Label с "{key} prio={N}").
  - cpp/template_loader.hpp/.cpp — JSON → vector<LayoutTemplate> через json-c.
    builtin_templates() = { tpl_1, tpl_4 } как fallback.

SourcePool:
  - cpp/source_pool.hpp/.cpp — owner cfc_source_t*, motion state атомарный,
    zone-filter в motion_pulse. Pool entries — non-copyable unique_ptr.

Composer:
  - cpp/composer.hpp/.cpp — owner SourcePool + templates + Layout + output.
    Алгоритмы: pick_best_fit (min nb_camera_cells >= need + priority tie-break),
    collect_active (drawable AND motion_within_TTL), asymmetric hysteresis
    (рост сразу через std::includes, сжатие — wait shrink_hysteresis_ms).
    Public C++ API: set_motion_mode / set_layout / load_templates / compose_frame.

ООП-гипотеза smoke:
  - examples/grid_record_cpp.cpp — минимальный smoke без NVENC. Init composer,
    compose_frame N раз, dump NV12 в файл. Проверяет что C++ модель
    компилируется, линкуется с C-кодом (source.c, nvenc.c остались на C через
    extern "C"), и реально рисует кадр.

Производительность сохранена:
  - Один output буфер VMM, передаётся как NV12Ref (read-write reference) во все
    cells/decorations — НИКАКИХ memcpy на cells boundary.
  - Virtual call overhead: 1 indirect call per cell per frame. Negligible.
  - Heap allocations только при apply_template (раз в N секунд при relayout).

Build:
  - CMakeLists.txt: CXX language, C++17.
  - src/CMakeLists.txt: COMPOSER_SOURCES_CPP добавлен в lib.
  - examples/CMakeLists.txt: grid_record_cpp.

Smoke test run jammy:
  [cfc/loader] docker/templates.json: loaded 7 templates
  [smoke] composer 1920x1080 templates=7 sources=0 motion=0
  [smoke] wrote 3317760 bytes (Y=2211840 UV=1105920) to /out/blank.nv12
  Build PASS, init PASS, compose PASS, dump PASS.

Что НЕ сделано:
  - extern "C" ABI shim для control.c / grid_record.c (старый C-композитор
    всё ещё единственный для prod stack).
  - Удаление старых composer.c / overlay.c / layouts.c.
  - Live deploy в прод (Step 1-3 функциональность).
  - JSON ZMQ hot-reload (был в Step 3 C-version, восстановить в C++).

Refs: #195 (Phase 11b C++ refactor).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-03 21:43:18 +01:00

178 lines
6.0 KiB
C++

/* grid_record_cpp — Phase 11b ООП-гипотеза.
*
* Минимальный smoke: проверяет что C++ модель Cell/Layout/Decoration/Composer
* компилируется, линкуется с C-частями, и реально рисует кадры через те же
* CUDA-kernels (zero-copy: единый NV12 буфер не копируется между cells).
*
* Делает N композиций → dump последнего NV12 кадра в файл → exit.
* Без NVENC: цель гипотезы — не encode, а доказать что ООП-pipeline работает.
*
* Использование:
* grid_record_cpp --out /tmp/last.nv12 --frames 50 \
* --templates /opt/templates.json \
* --source cam-parking,frigate=parking_overview,priority=100 \
* --source cam-back_yard,frigate=back_yard,priority=70 \
* --motion-mode
*/
#include "../include/cuframes_composer/cpp/composer.hpp"
#include <cuda.h>
#include <cuda_runtime.h>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <getopt.h>
#include <signal.h>
#include <string>
#include <thread>
#include <unistd.h>
#include <vector>
namespace {
volatile sig_atomic_t g_stop = 0;
void on_sig(int) { g_stop = 1; }
struct SourceSpec {
std::string key;
std::string frigate;
int priority = 0;
std::vector<std::string> zones;
};
std::vector<std::string> split_colon(const std::string& s)
{
std::vector<std::string> out;
std::string cur;
for (char c : s) {
if (c == ':') { if (!cur.empty()) out.push_back(cur); cur.clear(); }
else cur.push_back(c);
}
if (!cur.empty()) out.push_back(cur);
return out;
}
SourceSpec parse_source(const std::string& arg)
{
SourceSpec s;
std::vector<std::string> parts;
std::string cur;
for (char c : arg) {
if (c == ',') { parts.push_back(cur); cur.clear(); }
else cur.push_back(c);
}
if (!cur.empty()) parts.push_back(cur);
if (parts.empty()) return s;
s.key = parts[0];
for (std::size_t i = 1; i < parts.size(); i++) {
auto& p = parts[i];
if (p.rfind("frigate=", 0) == 0) s.frigate = p.substr(8);
else if (p.rfind("priority=", 0) == 0) s.priority = std::atoi(p.c_str() + 9);
else if (p.rfind("zones=", 0) == 0) s.zones = split_colon(p.substr(6));
}
return s;
}
} // namespace
int main(int argc, char** argv)
{
std::string out_path;
std::string templates_path;
int width = 1920, height = 1080;
int frames_to_compose = 25;
bool motion_mode = false;
int motion_ttl = 45000;
std::vector<SourceSpec> sources;
static struct option opts[] = {
{"out", required_argument, 0, 'o'},
{"frames", required_argument, 0, 'n'},
{"width", required_argument, 0, 'W'},
{"height", required_argument, 0, 'H'},
{"source", required_argument, 0, 'S'},
{"motion-mode",no_argument, 0, 'm'},
{"motion-ttl", required_argument, 0, 'k'},
{"templates", required_argument, 0, 'z'},
{0, 0, 0, 0},
};
int c;
while ((c = getopt_long(argc, argv, "o:n:W:H:S:mk:z:", opts, nullptr)) != -1) {
switch (c) {
case 'o': out_path = optarg; break;
case 'n': frames_to_compose = std::atoi(optarg); break;
case 'W': width = std::atoi(optarg); break;
case 'H': height = std::atoi(optarg); break;
case 'S': sources.push_back(parse_source(optarg)); break;
case 'm': motion_mode = true; break;
case 'k': motion_ttl = std::atoi(optarg); break;
case 'z': templates_path = optarg; break;
default: return 1;
}
}
if (out_path.empty()) {
std::fprintf(stderr, "Usage: %s --out FILE --source ... [--motion-mode]\n", argv[0]);
return 1;
}
signal(SIGINT, on_sig);
signal(SIGTERM, on_sig);
cuInit(0);
CUdevice dev; cuDeviceGet(&dev, 0);
CUcontext ctx; cuDevicePrimaryCtxRetain(&ctx, dev);
cuCtxPushCurrent(ctx);
cfc::ComposerConfig ccfg;
ccfg.width = width;
ccfg.height = height;
ccfg.templates_path = templates_path;
ccfg.motion_ttl_ms = motion_ttl;
cfc::Composer composer(ccfg);
if (!composer.ok()) {
std::fprintf(stderr, "[smoke] composer init failed\n");
return 1;
}
cfc::SourcePool::SubscribeOpts opts_sub;
for (auto& s : sources) {
composer.pool().add(s.key, s.frigate, s.priority, s.zones, opts_sub);
}
if (motion_mode) composer.set_motion_mode(true, motion_ttl);
std::fprintf(stderr, "[smoke] composer %dx%d templates=%d sources=%zu motion=%d\n",
width, height, composer.templates_count(), sources.size(),
motion_mode ? 1 : 0);
/* Несколько композиций — даём sources подключиться. */
cfc::NV12Ref last{};
for (int i = 0; i < frames_to_compose && !g_stop; i++) {
last = composer.compose_frame();
cudaStreamSynchronize(0);
std::this_thread::sleep_for(std::chrono::milliseconds(40));
}
/* Dump последнего кадра в файл. */
std::size_t y_size = static_cast<std::size_t>(last.pitch_y) * height;
std::size_t uv_size = static_cast<std::size_t>(last.pitch_uv) * (height / 2);
std::vector<unsigned char> host(y_size + uv_size);
cuMemcpyDtoH(host.data(), last.y_ptr, y_size);
cuMemcpyDtoH(host.data() + y_size, last.uv_ptr, uv_size);
FILE* f = std::fopen(out_path.c_str(), "wb");
if (!f) { std::fprintf(stderr, "[smoke] open '%s' failed\n", out_path.c_str()); return 1; }
std::fwrite(host.data(), 1, host.size(), f);
std::fclose(f);
std::fprintf(stderr, "[smoke] wrote %zu bytes (Y=%zu UV=%zu) to %s\n",
host.size(), y_size, uv_size, out_path.c_str());
std::fprintf(stderr, "[smoke] current template: '%s'\n",
composer.current_layout_name().c_str());
cuCtxPopCurrent(nullptr);
cuDevicePrimaryCtxRelease(dev);
return 0;
}