[Epic] vf-cuda-grid — implementation roadmap (Phases 1-6) #1
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Epic: vf-cuda-grid implementation
Tracking issue для 6 phases implementation plan'а.
Дизайн зафиксирован в
docs/design.md(1124 строки) — architect-reviewed.Phases (последовательно — каждая зависит от предыдущей)
Phase 1 — MVP filter (PR-1)
libavfilter/vf_cuda_grid.c--enable-vf-cuda-grid(chained на cuda-llvm? см. design §3)ffmpeg -i ... -filter_complex "[0][1][2][3]cuda_grid[out]" ...Acceptance: локально на R9 работает
ffmpeg -hide_banner -filter_complex "[0:v][1:v][2:v][3:v]cuda_grid[out]" -map "[out]" out.mp4с фиксированной quad сеткой.Phase 2 — Dynamic layouts + scaling (PR-2)
single,dual_horizontal,dual_vertical,quad,main_plus_preview,panoramic,six_grid,nine_grid,sixteen_gridlayout=<name>для выбора templateAcceptance: все 9 templates работают, переключение через rebuild filter graph (runtime будет в Phase 3).
Phase 3 — Sidecar controller (PR-3)
cuda-grid-controllerPython package (FastAPI + asyncio + pyzmq + aiomqtt)zmqfilter (см. design §7)POST /layout/{instance}/set,POST /layout/create,GET /state)select.layout,sensor.current_fps,binary_sensor.onlinelayout_switched,cell_camera_changed,fps_drop,audio_duckedprocess_commandподдержка дляset_layout,create_layout,delete_layoutAcceptance: через HA UI можно переключать layouts на live filter без teardown.
Phase 4 — Basic overlays (PR-4)
POST /overlay/add(returns overlay_id),DELETE /overlay/{id},PATCH /overlay/{id}frigate/+/motion+frigate/events→ overlays autoAcceptance: на live grid появляются bbox от Frigate detections + LPR text для проезжающих машин.
Phase 5 — Rich overlays (PR-5)
Acceptance: public stream показывает logo + weather widget, private stream — те же overlays + LPR text + motion timeline graph.
Phase 6 — Audio orchestration (PR-6)
transitionslibrary)amix+sidechaincompressorchestrationorchestration_rules.yaml)door_ring→ controller orchestrates audio+video transitionAcceptance: реальный setup в localhost-infra — physical doorbell или MQTT-кнопка вызывает full transition pipeline.
Связано
gx/cuframes— frame sourcegx/ffmpeg-patchedn7.1-cuframes — куда патчvf_cuda_grid.cпойдёт (или отдельный fork)gx/cctv#22Phase 4 — закроется после Phase 4 vf-cuda-grid + миграция cctv-processor (см. design §13)gx/cctv#24— superseded (см. design §14)Open для обсуждения (см. design §12 Risks)
Каждый Phase — отдельный PR. Подзадачи на дальнейших Phases можно decompose'ить в sub-issues по мере приближения.
Phase 1 — PR opened (draft)
gx/ffmpeg-patched PR #2 —
vf_cuda_grid.c(~285 LOC).Verified:
ffmpeg -filters | grep cuda_grid→VVVV->Vffmpeg -h filter=cuda_gridпоказывает 4 inputs + 1 outputLimitations Phase 1 (per design):
Acceptance для не-draft (нужно):
После acceptance — merge → Phase 1 checklist в epic закрывается → переходим на Phase 2 (Layout DSL + scaling).
Phase 2a — layout templates + dynamic nb_inputs ✅
PR-2 partial deliverable: commits
6ee2f47+df47647вgx/ffmpeg-patched n7.1-vf-cuda-grid.Что сделано:
single,dual_horizontal,dual_vertical,quad(default),main_plus_preview,six_grid,nine_grid,sixteen_grid,panoramicAVFILTER_FLAG_DYNAMIC_INPUTSflag +ff_append_inpad_free_nameвinit().layout=<name>,out_w=<int>,out_h=<int>(defaults: quad, 1920×1080)config_output(normalized × out_size), align до chroma boundaryVerify:
Phase 2a limitation (всё ещё не закрыто):
Дальше — Phase 2b: per-cell scaling через libnpp (
nppiResize_8u_C1Rдля Y,nppiResize_8u_C2Rдля UV interleaved). Это разблокирует mixed-size cameras иmain_plus_previewuse case (1 large cell 1280×1080 + 3 small cells 640×360 из 1080p sources).Phase 2a checked off в epic.
Phase 2b — scaling delegated to upstream
scale_npp✅После исследования NPP API обнаружено что
nppiResize_*не имеет_C2Rvariant для 2-channel interleaved (только_C1R,_C3R,_C4R). NV12 UV plane = 2-channel interleaved → in-filter scaling требовал бы либо:nppiResize_8u_C1Rс split/merge через intermediate buffersPragmatic decision (Unix philosophy): cuda_grid делает только composition. Scaling делегируется существующему production-tested
scale_nppfilter в filter chain:Controller (Phase 3) auto-generates filter graph с scale_npp per input на основе layout cell sizes. Это decoupling clean — каждый filter делает одну вещь, scale_npp полностью тестирован.
Trade-offs:
Filter теперь возвращает explicit error message с примером scale_npp chain если input size mismatch:
Phase 2 (2a + 2b) complete. Commits:
6ee2f47,df47647,178fc5bвn7.1-vf-cuda-grid. Дальше Phase 3 controller sidecar.Phase 3 —
cuda-grid-controllerPython sidecar ✅Commit
37232aeвmain—controller/directory (~700 LOC Python).Stack: Python 3.11+ / FastAPI / asyncio / aiomqtt / pyzmq / pydantic / structlog / typer.
Modules:
config.pylayouts.pyvf_cuda_grid.cPhase 2)ha_discovery.pyselect.layout+sensor.current_layout+binary_sensor.onlineper instancezmq_client.pyzmqfilter (с timeout + reset on fail)state.pymqtt_loop.pycuda_grid/cmd/+/+/+, publish state + events, LWT, HA status reconnect detectiondispatch.pylayout.setaction → ZMQ send + state update + eventshttp_api.py/health,/layouts,/state,POST /layout/{inst}/set__main__.pyEnd-to-end flow готов:
Examples + Docker:
examples/controller.yaml— 2 instances (livingroom_tv, public_stream) sampleDockerfile— python:3.11-slim, entrypoint cuda-grid-controllerREADME.md— usage, FFmpeg side filter graph примерHA dashboard entities (auto-discovery после startup):
select.cuda_grid_<instance>_layout— dropdown с 9 layoutssensor.cuda_grid_<instance>_current_layout— текущийbinary_sensor.cuda_grid_controller_onlinePhase 3 limitations (Phase 4+ роадмап):
process_commandreply parsing simple (Phase 4 будет proper error propagation)filter_targethardcoded в config (Parsed_cuda_grid_0)Next: Phase 4 — overlay primitives (rect/text/icon) с CUDA rendering в filter side + HTTP/MQTT API в controller'е (
POST /overlay/add). См. design §5.Phase 4a — overlay infrastructure (controller side) ✅
Commit
a1090a5вmain— +520 LOC Python (overlays + Frigate bridge + REST/MQTT API).Что сделано (controller-side, end-to-end except rendering):
overlays.pyrect,text,icon,image,dim,graph,chatчерез pydantic. Normalized coords (0.0-1.0), optional cell binding, z_order, opacity, visiblestate.pydispatch.pyhttp_api.pyPOST/GET/PATCH/DELETE /overlay/{inst}/{id}REST endpointsmqtt_loop.pycuda_grid/cmd/<inst>/overlay/add|remove|clearsubscribefrigate_bridge.pyfrigate/+/motion+frigate/events— мапит к target_instance + cell. Phase 4a: log only. Phase 4b: auto-generate overlaysAPI examples:
Frigate bridge config (
examples/controller.yaml):Сейчас bridge просто logs received motion/detection events. Phase 4b будет автоматически генерить overlay'ы (rect + label text для object detections, dim overlay для motion regions).
Phase 4a limitations:
Status v0.4 work:
Phase 4b завершена (filter-side)
Ночью 2026-05-18→19 implemented + compiled все 4 overlay primitive types через автономную работу.
Что сделано
Alpha_Fill_Y/UV)Изменения
ffmpeg-patched (branch
n7.1-vf-cuda-grid):libavfilter/vf_cuda_grid.c— +1100 LOC (overlay state, process_command, helpers)libavfilter/vf_cuda_grid.cu— NEW 100 LOC (4 CUDA kernels: Alpha_Fill_Y/UV + Alpha_Blit_RGBA_Y/UV)libavfilter/Makefile— wiredvf_cuda_grid.ptx.o+cuda/load_helper.oconfigure—cuda_grid_filter_deps_any="cuda_nvcc cuda_llvm"vf-cuda-grid (branch
main):controller/cuda_grid_controller/dispatch.py— switch JSON → key=val URL-encoded wire formattools/smoke_test_overlays.sh— manual integration test scriptDockerfile.debian12 (frigate-cuframes-test, local-only):
--enable-libzmq --enable-cuda-nvcc --nvccflags="-gencode arch=compute_75,code=sm_75 -O2"apt install libzmq3-devWire format
String values URL-encoded (text=hello%20world). Filter inline decode'ит
%xx.Examples:
rect_001 rect cell=0 x=0.1 y=0.1 w=0.3 h=0.3 r=255 g=0 b=0 thickness=4 opacity=255text_001 text cell=1 x=0.3 y=0.4 text=Hello%20World font_size=48 r=255 g=255 b=255 opacity=200icon_001 icon x=0.8 y=0.05 icon_name=domofon opacity=255Build verified
Image
ffmpeg-vf-cuda-grid:phase4b-icon(10 GB builder layer) собран. Smoke check:ffmpeg -h filter=cuda_gridпоказывает все options (layout/out_w/out_h/font_file/font_size/icon_dir)ffmpeg -filters | grep -E "cuda_grid|zmq"— оба filter'а presentЧто НЕ сделано
frigate_bridge.py) — events приходят + логируются, но auto-create overlay команды не вызываются. Это Phase 4b separate task.Gotchas задокументированы в cuframes-stack-build skill Recipe B2:
--enable-cuda-nvccобязателен (default = clang autodetect → "not found")cuda_grid_config_input.ptx.oобязателенLD_LIBRARY_PATHCommits
9deaca7→c5130cb(5 commits: 4b-1, 4b-2, configure, 4b-3, URL-decode, 4b-4)c396a47(wire format + smoke test script)77a3029(skill update)Live test PASSED 🎉
Утром 2026-05-20 GPU освободилась (7.5 GB free) — запустил smoke test, все 4 overlay типа реально рендерятся.
Setup:
hwupload_cuda→cuda_grid=layout=quad:out_w=1280:out_h=720→zmq=bind_address=tcp://0.0.0.0:5599→hwdownload,format=nv12→libx264→ mp4Commands отправлены (4× add_overlay):
Replies: все 4 —
0 Success+ok id=X n=N. Filter автоматически загрузил/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttfпри первом text overlay.Pipeline отработал 641 frames (64 sec), exit clean. Output mp4 = 65 KB. Frame extracted визуально подтверждает все 4 типа корректно рендерятся.
⚠️ Critical gotcha найден — ZMQ wire format
FFmpeg
zmqfilter parses черезav_get_token(libavfilter/f_zmq.c:100-115) — берёт ОДИН token как arg. Если arg содержит spaces (наш случай), нужно обернуть в кавычки:TODO для controller:
dispatch._serialize_overlay_to_zmqвозвращает unquoted строку,zmq_client.send_commandf-string не quote'ит args. Нужно добавить wrap в '...' в одном из этих мест.Visual verification
Output mp4:
/tmp/grid_with_overlays.mp4, frame:/tmp/grid_frame.png(на dev-машине localhost).Phase 4b формально closed. Следующее: Phase 4b auto-rendering в FrigateBridge + Phase 5 (audio) + Phase 6 (graphs/charts).
Phase 4b — FrigateBridge auto-rendering LIVE 🎉
Полная цепочка production-ready:
Frigate MQTT event →
FrigateBridge.handle_message→CommandDispatcher→_serialize_overlay_to_zmq→ ZMQ quoted → filterparse_overlay_args→ CUDA kernel render.Что добавлено
zmq_client.send_command— оборачиваетargsв single-quotes (escapes'→\'). FFmpeg'sav_get_tokenhonours quoting → arg идёт как single token.dispatch._serialize_overlay_to_zmq— translation layer pydantic → filter wire:color="#FF8800"r=255 g=136 b=0opacity=0.95opacity=242border_only=True, border_width=8thickness=8dim_factor=0.65amount=166text="Hello world"text=Hello%20worldFrigateBridgePhase 4b auto-overlay:frigate/<cam>/motion ON→ add RectOverlay border orange (id=motion_<cam>) на весь cellfrigate/<cam>/motion OFF→ remove тот overlayfrigate/events(new/update) → add RectOverlay (bbox normalized) + TextOverlay (label + score%)frigate/events(end) → remove обаcamera_width/camera_heightдля normalize bbox (default 1920×1080), флагиmotion_indicator/bbox_overlayFrigateBridgeпринимаетdispatcherв constructor → используетсяdispatcher.handle(instance, "overlay.add", json)для отправки.Live test verification
Симулировал 4 Frigate events (через direct method calls — bypass MQTT broker для test):
front_yard motion ON→ cell 0 orange border ✅gate_lprevent new car bbox @ middle → cell 1 green bbox + "car 87%" ✅back_yardevent new person → cell 2 bbox + "person 93%" ✅parking_overviewmotion ON + car event → cell 3 orange + bbox + "car 71%" ✅Все 8 overlays (2 motion + 3 bbox + 3 text) корректно rendered, bbox normalize правильно подсчитан с camera resolution per mapping.
Что осталось до production deploy
Commits:
96e6048— controller fixes + FrigateBridge auto-overlay (+229/-67 LOC)⚠️ Phase 5b/5c regression — REVERT к Phase 5a
Root cause: multi-source audio chain (astreamselect + amix + lavfi sine) в одном ffmpeg pipeline с RTSP push блокирует video pacing. Encoder log показывает 25 fps speed=1.0x, но TV consumer (192.168.88.36) видит ~0.5 fps. Audio sync ffmpeg muxer waits для frames на всех inputs (включая lavfi infinite generator); если хоть один MP3 stream буферизует, output stalls → encoder backlog → TV starves.
Verified диагностически:
Applied fix: revert к Phase 5a single audio inline (Europa Plus direct
-i+ AAC re-encode, без astreamselect/amix). Phase 6 dynamic overlays тоже disabled временно (под подозрением до отдельной verify когда audio решим).Memory зафиксировал:
feedback_audio-chain-blocks-video.md— правило + verification commandslocalhost-infra commit 3866728— applied changesPhase 5d — split-process architecture (новая phase)
Multi-source audio + ducking требуют отдельный ffmpeg process для audio path:
Так video pipeline не блокируется backpressure из audio chain. mediamtx умеет mux два published stream'а в один path.
Phase 6 dynamic overlays — re-enable после Phase 5d confirm (для уверенности что reload_icon не мerge stalls).
Commits:
3866728(revert + memory)e877a25(controller code не менялся — Phase 5b/5c controller endpoints оставлены, ждут split-process pipeline которая их consume'нет)