beb8e1baa0
Что в этом коммите:
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>
178 lines
6.0 KiB
C++
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;
|
|
}
|