35 Commits

Author SHA1 Message Date
gx 7f4bdfcaab Merge pull request 'Python bindings (pybind11) — Phase 0 v1' (#7) from feat/python-bindings into main
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Failing after 39s
build / ffmpeg filter patch (out-of-tree) (push) Has been skipped
2026-06-13 21:34:29 +01:00
gx afc2dd7fff python: DLPack + health stats + CUDA stream + docs (tasks #199-#202)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (pull_request) Failing after 1m50s
build / ffmpeg filter patch (out-of-tree) (pull_request) Has been skipped
#199 DLPack export:
- frame.dlpack_y() / .dlpack_uv() — explicit multi-plane access для NV12
- frame.__dlpack__() / __dlpack_device__() — protocol для torch/cupy
- Capsule deleter правильно держит refcount на frame_keep_alive,
  releases shape/strides arrays. CUDA pointer принадлежит frame.

#200 Health/stats counters:
- frames_received, timeouts, errors — per-call counters
- last_seq, gap_count — proxy для drop count (NEWEST_ONLY mode)
- last_frame_pts_ns
- stats() — snapshot dict для MQTT health publish
- counted в pybind layer т.к. C API не expose'ит ring_occupancy

#201 Per-subscriber CUDA stream + thread-safety:
- consumer_stream kwarg в subscribe() — int (cudaStream_t pointer)
- subscriber.consumer_stream property
- Thread-safety contract в docstring CuframesSubscriber
- next_frame() передаёт consumer_stream_ в cuframes_subscriber_next

#202 Smoke test + docs:
- 10/10 pytest passed (расширен +2 теста на consumer_stream)
- docs/python.md (~250 строк): quick start, API reference, integration
  с PyTorch/CuPy, reconnect-loop pattern, per-stream usage,
  pitch alignment, thread-safety, error taxonomy, backpressure,
  Phase 0 limitations

Verify build + tests:
  cmake -B build-python -DBUILD_PYTHON_BINDINGS=ON
  cmake --build build-python -j
  pytest python/tests/ -v   # 10/10

Закрывает Phase 0 issue gx/cuframes#6.
Разблокирует goldix-smart-home/yolo-world-detector Phase 1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 21:33:21 +01:00
gx 5d1eaedb38 python: CuframesSubscriber + CuframesFrame wrapper (task #198)
Реализует subscriber-side wrapper над cuframes_subscriber_* и
cuframes_frame_* C API.

Что добавлено:
- CuframesFrame — owning RAII wrapper над cuframes_frame_t*
  - properties: cuda_ptr, format, width, height, pitch_y, pitch_uv,
    seq, pts_ns, released
  - release() idempotent
  - context manager (__enter__/__exit__) — release при выходе
  - после release() property access бросает CuframesError

- CuframesSubscriber — owning RAII wrapper над cuframes_subscriber_t*
  - конструктор с key/consumer_name/mode/cuda_device/connect_timeout_ms
  - next_frame(timeout_ms) → CuframesFrame
  - close() idempotent
  - context manager
  - GIL released на блокирующих вызовах (create, next_frame)

- subscribe() — module-level factory shortcut

Архитектурные решения:
- GIL release в py::gil_scoped_release на subscriber_create и _next —
  чтобы другие Python потоки могли работать пока ждём frame
- consumer_stream передаётся как nullptr в Phase 0 (default stream);
  per-subscriber stream в task #201
- Frame держит raw pointer на subscriber, refcount Python-стороной;
  если subscriber уничтожен раньше, frame.release() становится no-op

Smoke tests расширены до 8 — добавлены проверки exposed API и
error mapping на subscribe к несуществующему publisher'у.

Verify: pytest tests/test_smoke.py — 8/8 passed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 21:23:42 +01:00
gx 7b6d43efeb python: fix exception hierarchy — не вызывать .attr("__class__")
py::exception<T>(...) уже возвращает Python class object. Дополнительный
.attr("__class__") давал metaclass (type), из-за чего issubclass()
проверка для всех subexc возвращала False.

Verify: pytest tests/test_smoke.py — 5/5 passed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 21:19:03 +01:00
gx a7da4ea728 python: skeleton pybind11 bindings (issue #6 task #197)
Каркас Python-пакета `cuframes`:
- python/pyproject.toml — scikit-build-core конфиг
- python/CMakeLists.txt — pybind11 module через FetchContent
- python/src/_native.cpp — module entry, error таксономия,
  enum mirrors (PixelFormat, SubscriberMode), version
- python/cuframes/__init__.py — re-export публичного API
- python/tests/test_smoke.py — smoke tests без real subscribe
- python/README.md — статус + build instructions
- CMakeLists.txt — подключение python/ при BUILD_PYTHON_BINDINGS=ON

Реальный subscriber/frame wrapper в следующих коммитах
(tasks #198-#202).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-13 12:59:04 +01:00
gx 655649f4d8 cmake: использовать PROJECT_SOURCE_DIR вместо CMAKE_SOURCE_DIR
build / cmake build (CUDA 12.4, Ubuntu 22.04) (pull_request) Failing after 5m19s
build / ffmpeg filter patch (out-of-tree) (pull_request) Has been skipped
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Failing after 4m14s
build / ffmpeg filter patch (out-of-tree) (push) Has been skipped
При сборке cuframes как подпроекта родительского CMake-проекта
(add_subdirectory) CMAKE_SOURCE_DIR указывает на корень родителя,
а не cuframes. Из-за этого target_include_directories cuframes
получал неверный путь и компиляция падала с

  fatal error: cuframes/cuframes.h: No such file or directory

PROJECT_SOURCE_DIR резолвится в каталог project(), то есть всегда
указывает на корень cuframes независимо от способа подключения.

Standalone-сборка ведёт себя как раньше — оба пути одинаковы.
2026-06-03 04:27:24 +01:00
Claude Opus 78824c4ed1 docker: +mosquitto-clients в runtime image
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 1m42s
build / ffmpeg filter patch (out-of-tree) (push) Failing after 1m22s
Нужен для loop-publisher.sh wrapper в cctv stack — heartbeat и alert MQTT
publish. 4.5 MB добавил, runtime image теперь ~590 MB. Без него wrapper
silent fail на mqtt_alert/mqtt_state (но retry-loop работает).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 17:59:56 +01:00
gx 4862247fe2 v0.4: VMM + POSIX FD — namespace decoupling (no pid share required)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 1m46s
build / ffmpeg filter patch (out-of-tree) (push) Failing after 1m30s
Заменяет cudaMalloc + cudaIpcGetMemHandle на cuMemCreate (VMM) +
cuMemExportToShareableHandle(POSIX_FILE_DESCRIPTOR). FDs передаются consumer'у
через sendmsg(SCM_RIGHTS) в handshake. Frigate (s6-overlay не даёт share PID)
и любой другой consumer работают БЕЗ pid namespace share — только volume mount
unix socket'a /run/cuframes и IPC share для /dev/shm header.

Sync: cudaEventRecord+IPC events → cuStreamSynchronize в do_publish.
Producer ждёт ~1 ms что stream flush'нулся, потом atomic_store(seq).
Consumer читает seq через memory_order_acquire и копирует DtoD без
event wait — HW coherence гарантирована на одном GPU.

ABI break (согласован с user'ом):
  - magic 0xCC7C1DCC → 0xCC7C1DCE (старые consumers fail cleanly)
  - protocol V3 → V4
  - libcuframes.so.0 SOVERSION остаётся, но .so.0.3.0 → .so.0.4.0
  - EXTERNAL ownership убран (VMM требует cuMemCreate-allocated memory,
    нельзя export'нуть произвольный cudaMalloc-pointer как POSIX FD)
  - cuframes-rtsp-source переведён на LIBRARY mode + один D2D memcpy
    в acquire'нутый slot (overhead малый — публишер всё равно делал такой
    D2D из FFmpeg hwframe pool в EXTERNAL pool раньше)

Размер: granularity 2 MB на 5090 → NV12 1920×1080 (~3.1 MB) округляется до
4 MB, +1 MB на slot × 16 × 4 камеры = +64 MB VRAM. Терпимо.

Packet ring (cuframes_packets://) НЕ затронут — отдельный SHM с своим
magic, работает как раньше.

PoC + smoke в spike/:
  - vmm_fd_pingpong/ — minimal cuMemCreate+FD round-trip
  - smoke_v04/ — full publisher+subscriber, 100/100 frames без pid share

Base image: Dockerfile.runtime → CUDA 12.4 (был 13.0). Matching prod
pipeline + Frigate base, иначе libcudart conflict при load.

Compose stack (localhost-infra repo) — параллельный commit:
  - убран pid: container:cuframes-pub-parking из subscribers
  - image теги: gx/cuframes:0.4, gx/cuda-grid-pipeline:phase8,
    gx/frigate:cuframes-v0.4

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 20:13:31 +01:00
gx d646f5a4e4 v0.3.3: consumer post-sync verify даже для v0.3 per-slot events
release / build runtime Docker image (push) Failing after 0s
release / build source tarball (push) Successful in 4s
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 1m41s
build / ffmpeg filter patch (out-of-tree) (push) Successful in 1m29s
test-u4-runner / u4 runner smoke test (push) Has been cancelled
Bug: cudaEventRecord(event[slot]) overwrites previous state каждый publish.
Когда producer wraps ring (~640ms при ring=16), event[slot] re-recorded для
new content. Consumer's pending cudaStreamWaitEvent satisfied новым signal —
consumer reads slot[slot_idx] thinking it's target_seq, реально получает
seq+ring_size content (stale-by-1-wrap drift).

После 50k+ wraps в long-running pipeline (9h uptime) drift накапливается:
output stream имеет 60-70% duplicate frames (vs 10% сразу после restart).

Симптом: TV picture freezes на 1-2 sec периодически. Encoder fps=25 stable
(content duplicates same PTS-advance), но motion choppy на 8-9 fps real.

Fix: unconditional post-sync verify (atomic re-read slot.seq после event wait).
Если producer wrap occurred — slot.seq != target_seq → continue к новому
target_seq. Cheap (one atomic load), correctness > perf.

Verified: после deploy с fresh pipeline, 18-sec sample = 4% duplicates
(vs 8.4% при том же setup но без fix).

Proper v0.4 fix: per-slot+per-publish event pool с unique handle per cycle.
Текущий v0.3.3 — sufficient mitigation для current production scale.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 20:27:00 +01:00
gx becfbebc78 cuframes-rtsp-source: + --policy + --ack-timeout-ms CLI flags
release / build runtime Docker image (push) Failing after 0s
release / build source tarball (push) Successful in 2s
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 1m39s
build / ffmpeg filter patch (out-of-tree) (push) Successful in 1m25s
test-u4-runner / u4 runner smoke test (push) Has been cancelled
Opt-in для STRICT_WAIT policy (default остаётся DROP_OLDEST).

Use case STRICT_WAIT:
  Frame integrity критичен (e.g. recording, frame-accurate analytics).
  Producer ждёт ack от всех subscribers перед wrap ring → no torn frames.
  Trade-off: slow consumer задерживает all (default 200ms timeout затем
  subscriber dropped from bitmap).

Use case DROP_OLDEST (default):
  Low-latency real-time display (TV grid). Producer wraps freely; v0.3
  per-slot CUDA events закрывают race без waiting.

Validation: policy=wait + ack-timeout-ms<=0 = infinite hold dead consumer —
warning + force к 200ms safe default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 08:47:14 +01:00
gx 656e36e9b0 v0.3.1: per-subscriber monitor thread — fix bitmap leak
release / build runtime Docker image (push) Failing after 0s
release / build source tarball (push) Successful in 4s
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 1m39s
build / ffmpeg filter patch (out-of-tree) (push) Successful in 1m32s
test-u4-runner / u4 runner smoke test (push) Has been cancelled
Bug: handshake_subscriber assigned bit + activated slot но НЕ tracked
client_fd. Когда subscriber container exited, socket closed on client side
но producer не detected → bit оставался set forever → после 32 connections
subscribe_create('cam-X'): too many subscribers (max 32).

Симптом в production: каждый pipeline recreate accumulated 1 stale subscriber.
После 4-5 recreate операций publishers перестали accept new pipeline →
"too many subscribers" crash loop.

Fix: после успешного handshake spawn detached pthread monitoring socket
via blocking recv(). recv() returns 0 (EOF) когда other side closes —
monitor clears bit (subscriber_bitmap &= ~(1<<bit)) + state[bit] = 0,
closes fd, exits.

Cost: 1 thread per active subscriber. Max 32 threads — небольшой
overhead. Threads detached, no join needed.

Stress test: 5x pipeline recreate без single "too many subscribers" error.
Раньше: 2-3 recreate → bitmap overflow.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 08:00:41 +01:00
gx 8c7abbc4e8 v0.3: per-slot CUDA events — закрывает TOCTOU race без crutches
release / build runtime Docker image (push) Failing after 1s
release / build source tarball (push) Successful in 5s
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 1m40s
build / ffmpeg filter patch (out-of-tree) (push) Successful in 1m22s
test-u4-runner / u4 runner smoke test (push) Has been cancelled
Protocol bump V2→V3:
  + shm header: cudaIpcEventHandle_t slot_event_handles[CUFRAMES_MAX_RING]
  + producer creates ring_size events (вместо одного global)
  + producer.do_publish records event[slot] (вместо pub->event)
  + consumer opens all slot events при subscribe
  + consumer waits event[slot_idx] specifically (вместо global producer_event)

Backward compat:
  - Legacy pub->event сохранён + ipc_event_handle export'ится — v0.2 consumers
    видят его и работают по-старому (с post-sync verify hack из 517107d).
  - v0.3 consumer auto-detects proto_version >= 3, fallback к legacy если
    cudaIpcOpenEventHandle на slot fail (graceful degradation).

Effect (15-sec sample на Phase 7 single-cam, motion):
  v0.1 production:  dup runs 34.7%, max 14 frames (560ms freeze)
  v0.2.1 fix:       dup runs 10%, max 6, 0 back-jumps detected
  v0.3 per-slot:    dup runs 1.9%, max 5, 3 back-jumps (likely encoder
                    static-content artifacts, not real race)

Размер shm header: 7424 → 8448 bytes (+1024 для slot_event_handles).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 09:23:53 +01:00
gx 517107d741 libcuframes: fix TOCTOU race в consumer slot read
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 1m34s
build / ffmpeg filter patch (out-of-tree) (push) Successful in 1m19s
release / build runtime Docker image (push) Failing after 1s
release / build source tarball (push) Successful in 4s
test-u4-runner / u4 runner smoke test (push) Has been cancelled
Bug: producer signals **один global** cudaEvent для всего ring (один на
producer). Consumer waits этот event после slot_seq validation, но event
соответствует ПОСЛЕДНЕМУ published frame, не slot[target_seq]. Если
producer wrap'нет ring во время event wait (ring=6 = 240ms окно), slot
содержит уже next-gen data, consumer возвращает torn/stale frame.

Симптом в production: video stream показывает «back-jump на момент»
periodically — camera OSD timestamp дёргается, motion machines briefly
teleport назад. cluster md5 analysis НЕ ловит (содержимое frames всё ещё
unique, просто из неправильной epoch).

Fix: post-sync verify. После cudaStreamWaitEvent / cudaEventSynchronize
re-check slots[slot_idx].seq == target_seq. Если producer перезаписал —
continue outer loop с новым target_seq.

Закрывает race window между slot validation и event sync return. Остаются
открытыми:
  - downstream GPU access после frame fill (consumer-side) — producer
    может wrap во время этого. Mitigation: STRICT_WAIT policy в publisher
    + ack discipline в consumer (cuframes_release_frame ack уже works).
  - bigger ring size снижает wrap frequency (240ms → 1.2s при ring=30).

Test: после deploy в cuda-grid-pipeline (Phase 7 single cam), camera OSD
clock больше не дёргается (раньше дёргалось каждые ~16 sec).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 22:27:39 +01:00
gx 4d54173bb2 roadmap: vf_cuda_grid выделен в отдельный продукт gx/vf-cuda-grid 2026-05-19 20:39:47 +01:00
gx 52fb2ad722 benchmarks: actual measured VRAM + network bandwidth (tcpdump-based)
VRAM breakdown (nvidia-smi pmon):
- 4 publishers = 4.4 GB (FHD + 2688x1520 ring buffers + NVDEC)
- cctv-backend = 1.0 GB
- frigate embeddings_manager = 1.6 GB
- frigate detector:onnx = 0.6 GB
- Total cuframes-stack = ~7.7 GB

Network (10-sec tcpdump capture от camera subnet к R9):
- Measured: 31.5 Mbps (всё включая go2rtc on-demand, ONVIF)
- cuframes core: ~16 Mbps (4 publishers × main HEVC)
- ONVIF/RTSP keepalives: ~1-2 Mbps
- Без cuframes setup тех же 4 cam × 3 consumer был бы ~45-50 Mbps

Source: production deploy 2026-05-19 measurement.
2026-05-19 19:22:53 +01:00
gx 3779175737 docs(benchmarks): production v0.2 deploy metrics (4 cam × 3 consumer)
Real-world numbers с production deploy 2026-05-19:
- RTSP к камерам: 12 → 4 (−67%)
- NVDEC sessions: 8 → 4 (−50%)
- Camera bandwidth: 34 → 16 Mbps (−54%)
- PCIe D2H copies: 346 MB/s → ~0 (−100% через zero-copy CUDA IPC)
- Frigate прямые RTSP: 8 → 0 (−100%)

Plus live nvidia-smi metrics, что сохранилось vs не сэкономлено,
projection table для других setup'ов (8/16 cam × 2/3/4 consumer).

Для promotional material — public-facing claims на основе measured deploy.
2026-05-19 19:07:16 +01:00
gx 98d1bb5296 release: v0.2.0 — encoded packet ring
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Failing after 3m3s
test-u4-runner / u4 runner smoke test (push) Successful in 1s
build / ffmpeg filter patch (out-of-tree) (push) Has been skipped
release / build runtime Docker image (push) Failing after 5m58s
release / build source tarball (push) Successful in 6m2s
- CHANGELOG: [Unreleased] → [0.2.0] — 2026-05-19
- CMakeLists VERSION 0.1.0 → 0.2.0 (both root + libcuframes)
- CUFRAMES_VERSION_MINOR: 1 → 2 в include/cuframes/cuframes.h

См. issue #2 (closed) + PR #4 (merged).
2026-05-19 17:49:14 +01:00
gx 5536d23992 Merge pull request 'v0.2: encoded packet ring' (#4) from v0.2-encoded-packets into main
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 10m0s
build / ffmpeg filter patch (out-of-tree) (push) Successful in 8m32s
2026-05-19 17:47:10 +01:00
gx 2b94742df4 ci: retry + explicit Node 20 version check в bootstrap
build / cmake build (CUDA 12.4, Ubuntu 22.04) (pull_request) Successful in 6m24s
build / ffmpeg filter patch (out-of-tree) (pull_request) Successful in 6m21s
Symptom (run #1826 fail на u4-runner):
  Bootstrap step молча установил Node 12 (Ubuntu default) вместо Node 20
  из NodeSource → actions/checkout@v4 не парсится (ES2022 static blocks).

Cause:
  curl ... setup_20.x на slow network (u4 через VPN) timeout/fail silently,
  apt install fallback на default ubuntu nodejs (Node 12). Без error.

Fix:
  - curl --retry 3 --retry-delay 5 --connect-timeout 30
  - retry-loop на NodeSource setup (3 попытки)
  - явная verification major version >= 18 после install, fail с exit 1
    если установился Node < 18

Применяется к обоим jobs (cmake-build и filter-build).

Связано: PR #4 (v0.2), run #1826 fail.
2026-05-19 17:31:33 +01:00
gx fca07bf669 test+docs: packet ring stress test + Frigate dual-input guide (v0.2 Step 6)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (pull_request) Failing after 3m43s
build / ffmpeg filter patch (out-of-tree) (pull_request) Has been skipped
Тесты:
- libcuframes/tests/test_packet_ring.c — 2 scenarios:
  1) normal flow: 1 pub × 1 sub × 2000 packets, varied sizes, GOP=30,
     payload integrity check (seq в первых 8 байтах + pattern). PTS
     monotonicity, first KEY seq, нет data errors.
  2) slow consumer (10ms delay): publisher 200 fps, subscriber должен
     detect OVERRUN, library resync на keyframe — verify received >10
     даже на сильно медленном консьюмере.
- libcuframes/tests/CMakeLists.txt: add_test packet_ring_basic.

Docs:
- CHANGELOG.md: новая [Unreleased] секция с full v0.2 highlights и
  явно declared limitations (sub-stream, audio, codec change → v0.3).
- docs/integrations/frigate.md: новая секция "v0.2: dual-input (detect +
  record через один RTSP)" с config example, requirements, trade-offs.

Связано: #2, PR #4. Step 6 (final) перед снятием draft.
2026-05-19 17:08:17 +01:00
gx 8cd96721ff feat(rtsp-source): packet ring publishing (v0.2 Step 4)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (pull_request) Successful in 1m39s
build / ffmpeg filter patch (out-of-tree) (pull_request) Successful in 1m44s
- cuframes::Publisher (C++ wrapper): добавлены enable_packets(),
  set_codec_extradata(), publish_packet() методы.
- cuframes-rtsp-source: новый CLI flag --enable-packet-ring. При его
  установке после opening stream — pub.enable_packets(codec_id) +
  set_codec_extradata из vstream->codecpar->extradata.
- В main loop: после av_read_frame, до avcodec_send_packet, packet
  публикуется в packet ring с конверсией pts/dts из stream_tb в ns,
  AV_PKT_FLAG_KEY/CORRUPT/DISCONTINUITY → CUFRAMES_PKT_FLAG_*.

Тест:
  cuframes-rtsp-source --rtsp rtsp://... --key cam1 --enable-packet-ring
  # frames consumer'ы продолжают работать через cuframes:// (как v0.1)
  # record consumer'ы могут brать packets через cuframes_packets:// (Step 5)

Связано: #2, PR #4.
2026-05-19 16:45:29 +01:00
gx 4cb0321a6f feat(api): public C API для packet ring (v0.2 Step 3)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (pull_request) Successful in 1m36s
build / ffmpeg filter patch (out-of-tree) (pull_request) Successful in 1m24s
Публичные функции в include/cuframes/cuframes.h:
- cuframes_publisher_enable_packets(opts)  — активирует ring на
  существующем publisher'е; default sizing (64 slots, 8MiB data, 2MiB max).
- cuframes_publisher_set_codec_extradata(data, size) — SPS/PPS bytes.
- cuframes_publisher_publish_packet(data, size, pts, dts, flags)
- cuframes_subscriber_enable_packets()  — открывает packet shm у subscriber'а.
- cuframes_subscriber_next_packet(pkt_out, timeout_ms) с поллингом 1ms.
- cuframes_packet_data/size/pts/dts/flags/seq accessors.
- cuframes_subscriber_release_packet()
- cuframes_subscriber_get_codec_params()

Internal:
- producer.c: расширена struct cuframes_publisher (has_pkt_ring,
  max_packet_size, pkt_ring); cleanup в destroy(); enable_packets()
  bump'ит proto_version=2 в frames header.
- consumer.c: расширена struct cuframes_subscriber (has_pkt_ring,
  pkt_ring, last_packet_seq, packet_obj); single-packet pattern (как
  frame_obj — busy flag, переиспользование buffer). enable_packets()
  стартует с last_keyframe_seq-1 для late subscriber resync. На
  PACKET_OVERRUN автоматически resync на last_keyframe и возвращает
  ERR наружу для signalling discontinuity.

Связано: #2, PR #4.
2026-05-19 16:27:05 +01:00
gx bd7fd95fef feat(libcuframes): packet ring buffer implementation (v0.2 Step 2)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (pull_request) Successful in 1m37s
build / ffmpeg filter patch (out-of-tree) (pull_request) Successful in 1m21s
Реализация encoded packet ring per docs/protocol.md §10.

Files:
- internal.h: cuframes_pkt_slot_t (64b packed), cuframes_pkt_header_t
  (0x1040 fixed header), cuframes_pkt_ring_t handle, constants for
  default sizing, packet flags, helper inline functions for slot/data
  pointer arithmetic.
- packet_ring.c (new, ~290 LOC): create/open/publish/read/destroy.
  Stale recovery симметрично frames SHM (pid liveness check). Seqlock
  pattern для subscriber защиты от overrun mid-read (post-check seq
  после copy). Wraparound memcpy helpers для variable-length data ring.
- utils.c: cuframes_internal_pkt_shm_name helper + strerror entries.
- cuframes.h: 4 новых error codes (PACKET_OVERSIZED, NO_PACKET_RING,
  NO_CODEC_PARAMS, PACKET_OVERRUN).
- CMakeLists.txt: src/packet_ring.c в sources.

API внутренний (cuframes_internal_pkt_ring_*) — publicly exposed
функции будут в Step 3 (cuframes.h API extension).

Связано: #2 (v0.2), PR #4 (draft).
2026-05-19 16:11:42 +01:00
gx ad75aa9624 docs(protocol): v0.2 — encoded packet ring spec (§10)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (pull_request) Successful in 1m35s
build / ffmpeg filter patch (out-of-tree) (pull_request) Successful in 1m39s
Полный wire-protocol spec для encoded packet ring:
- Отдельный SHM /dev/shm/cuframes-<key>-packets (variable-length)
- Backward-compat с v1: proto_version=2 publishers принимают v1 subscribers
- HELLO_REQ/HELLO_RESP extension через reserved bytes — без слома v1 layout
- Codec extradata (SPS/PPS) в shared header
- Late subscriber → keyframe-aligned start (initial_packet_seq)
- Seqlock pattern для защиты от overrun mid-read
- API extension: publish_packet, next_packet, get_codec_params
- 4 новых error codes (OVERSIZED, NO_PACKET_RING, NO_CODEC_PARAMS, PACKET_OVERRUN)

Связано: #2
2026-05-19 16:04:00 +01:00
gx 264b9d59db roadmap: future ideas — gst-cuframes-src + vf_cuda_grid
Две идеи добавлены в новую секцию "Future ideas" (без ETA):

- gst-cuframes-src: GStreamer source-element для DeepStream / обычных
  GStreamer pipeline'ов. Аналог FFmpeg-демуксера для другого стека.

- vf_cuda_grid: FFmpeg filter с runtime grid composition полностью
  на GPU. Заменяет custom C++ GridComposer cctv-processor (см. gx/cctv#22).
  Превращает cuframes в GPU-native video routing platform.

Обе идеи waiting на планирование, scope для v0.5+.
2026-05-19 15:58:49 +01:00
gx d2bae7d0fd ci: clone ffmpeg-patched через GITHUB_SERVER_URL (для VPN-runner'а)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 1m57s
build / ffmpeg filter patch (out-of-tree) (push) Successful in 3m36s
Жёсткий URL git.goldix.org не работает на u4-runner — там
gitea доступен только через VPN (10.8.0.6:3222). Используем
переменную runner'а — на R9 = 192.168.88.23:3222, на u4 = 10.8.0.6:3222.
2026-05-19 02:55:14 +01:00
gx eb3c058341 ci: smoke test workflow для verify u4 runner через VPN
test-u4-runner / u4 runner smoke test (push) Successful in 54s
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 38m52s
build / ffmpeg filter patch (out-of-tree) (push) Failing after 1m34s
2026-05-19 02:12:38 +01:00
gx 612843bd39 docs: launch drafts (Frigate discussion + FFmpeg-devel RFC + Show HN)
3 черновика для upstream visibility (Etap E):
- docs/launch/frigate-integration-issue.md — Discussion на blakeblackshear/frigate
- docs/launch/ffmpeg-devel-rfc.md — RFC patch + cover letter для ffmpeg-devel ML
- docs/launch/hn-show-post.md — Show HN draft (Etap F)
- docs/launch/README.md — порядок, чек-лист, pre-flight notes

См. issue #3.
2026-05-19 02:04:42 +01:00
gx bcc1d29ae8 ci: clone FFmpeg из local gitea fork (вместо unstable upstream github clone)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 1m52s
build / ffmpeg filter patch (out-of-tree) (push) Successful in 1m31s
git clone github.com/FFmpeg/FFmpeg на слабом интернете оборвался через 11 мин
(RPC HTTP/2 CANCEL). Local gx/ffmpeg-patched n7.1-cuframes branch имеет
patch уже applied — clone instant без internet round-trip.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:40:40 +01:00
gx fbe1d18c39 docs: troubleshooting guide + production notes
- docs/troubleshooting.md — 13 секций с реальными grабельками которые мы
  прошли: cudaIpcOpenEventHandle invalid device context (pid namespace),
  s6-overlay vs pid share, scale_cuda missing (cuda-llvm + stdbit.h glibc 2.36),
  libcuframes not found install paths, ffbuild/ missing source, GMP no working
  compiler (long-long reliability), zlib.net deprecated URL, RTSP/RTP UDP
  docker NAT, gitea actions Node version
- docs/architecture.md — Appendix A "Production deployment notes" с реальными
  observations после 24h+ run: что подтвердилось, что доработали, что не учли
- docs/requirements.md — production deployment matrix + Docker namespace
  requirements таблица (cross-container CUDA IPC требует 5 условий)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:37:13 +01:00
gx 022a198c33 ci: same Node 20 bootstrap для filter-build job (как в cmake-build)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 13m20s
build / ffmpeg filter patch (out-of-tree) (push) Failing after 18m48s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 00:05:59 +01:00
gx 611918ce7a ci: install Node 20 from NodeSource (apt nodejs = Node 12 — слишком старый для actions/checkout@v4)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Successful in 1m48s
build / ffmpeg filter patch (out-of-tree) (push) Failing after 51s
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:56:33 +01:00
gx 00fb3e9528 ci: preinstall node+git в CUDA container (actions/checkout требует node)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Failing after 1m6s
build / ffmpeg filter patch (out-of-tree) (push) Has been skipped
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:47:25 +01:00
gx 4a6a6f4a6c ci: gitea Actions workflows (build, release) + README badges
build / cmake build (CUDA 12.4, Ubuntu 22.04) (push) Failing after 1m4s
build / ffmpeg filter patch (out-of-tree) (push) Has been skipped
- .gitea/workflows/build.yml — on push/PR:
    * cmake build на CUDA 12.4 devel image (Ubuntu 22.04 base)
    * compile-only smoke (no GPU нужен): libcuframes.so + tools + examples
    * install-prefix layout verify (headers + libs в правильных путях)
    * filter/ — clone FFmpeg n7.1 + apply patch + build minimal patched
      ffmpeg, verify cuframes demuxer registered

- .gitea/workflows/release.yml — on tag v*:
    * build runtime Docker image, push в git.goldix.org/gx/cuframes:<version>
    * build source tarball cuframes-<version>.tar.gz как artifact

- README.md badges: build status, release version, license

Runner: gitea act_runner v0.4.1 на R9-88.23 — labels ubuntu-22.04 / ubuntu-24.04
доступны через docker.gitea.com/runner-images. CUDA devel image использует
nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04 (уже cached на runner host).

Stress test (требует GPU) намерено НЕ в CI — runner без GPU. Запускать
отдельно на dev-машине через ctest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:43:55 +01:00
gx 12708618d4 docs: reference integrations + examples
- docs/integrations/frigate.md — полный production-tested guide:
  Dockerfile, docker-compose, config.yml, troubleshooting (s6+pid, scale_cuda,
  hwaccel issues), build steps
- docs/integrations/cctv-cpp.md — C++ pattern: IFrameSource interface +
  CuframesSource skeleton + CMake setup + runtime requirements
- examples/frigate-compose/ — reference compose stack (cuframes-pub + Frigate)
  с config.yml stub, .env.example, README
- examples/python-consumer/ — ctypes-based skeleton для AI/ML pipeline'ов
  (до v0.3 native pybind11 bindings)
- docs/integration.md — превратился в index-страницу, ссылается на specific guides

Reorganization упрощает onboarding: пользователь выбирает guide по типу
integration'а (Frigate/C++/Python/FFmpeg) и сразу видит реальный code.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:37:35 +01:00
55 changed files with 6530 additions and 506 deletions
+181
View File
@@ -0,0 +1,181 @@
name: build
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
- 'BENCHMARKS.md'
- 'ROADMAP.md'
- 'CHANGELOG.md'
- 'LICENSE'
- '.gitea/ISSUE_TEMPLATE/**'
pull_request:
branches: [main]
jobs:
cmake-build:
name: cmake build (CUDA 12.4, Ubuntu 22.04)
runs-on: ubuntu-22.04
container:
image: nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04
steps:
# actions/checkout@v4 требует Node 20+. Ubuntu 22.04 apt даёт Node 12 — не подходит.
# Ставим Node 20 из NodeSource repo.
- name: Bootstrap Node 20 + git (для actions/checkout)
run: |
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends curl git ca-certificates gnupg
# NodeSource setup может молча упасть на slow networks (особенно через VPN
# на u4-runner); retry + явная verification что Node >= 18 после install.
for i in 1 2 3; do
if curl -fsSL --retry 3 --retry-delay 5 --connect-timeout 30 \
https://deb.nodesource.com/setup_20.x | bash -; then
break
fi
echo "NodeSource setup attempt $i failed, retrying..."
sleep 10
done
apt-get install -y --no-install-recommends nodejs
NODE_VER=$(node --version)
echo "node: $NODE_VER"
# actions/checkout@v4 требует Node 20+ (ES2022 static blocks).
# Если NodeSource setup упал и установился Ubuntu's Node 12 — фейлим явно.
NODE_MAJOR=$(echo "$NODE_VER" | sed -E 's/^v([0-9]+).*/\1/')
if [ "$NODE_MAJOR" -lt 18 ]; then
echo "ERROR: Node $NODE_VER too old, NodeSource setup likely failed" >&2
exit 1
fi
- name: Install build deps
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get install -y --no-install-recommends \
build-essential cmake ninja-build pkg-config \
libavformat-dev libavcodec-dev libavutil-dev libswscale-dev
- name: Checkout
uses: actions/checkout@v4
- name: Configure (full — libcuframes + examples + tools)
run: |
cmake -B build -S . -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTING=OFF \
-DBUILD_EXAMPLES=ON \
-DBUILD_TOOLS=ON \
-DBUILD_FFMPEG_FILTER=OFF \
-DBUILD_PYTHON_BINDINGS=OFF
- name: Build
run: cmake --build build --parallel
- name: Verify produced binaries + library
run: |
ls -la build/libcuframes/libcuframes.so*
ls -la build/libcuframes/libcuframes_static.a
ls -la build/tools/cuframes-rtsp-source/cuframes-rtsp-source
ls -la build/examples/sub_count/sub_count
./build/tools/cuframes-rtsp-source/cuframes-rtsp-source --help | head -5
- name: Install + verify install layout
run: |
cmake --install build --prefix /tmp/cuframes-install
test -f /tmp/cuframes-install/include/cuframes/cuframes.h
test -f /tmp/cuframes-install/include/cuframes/cuframes.hpp
test -f /tmp/cuframes-install/lib/libcuframes.so
test -f /tmp/cuframes-install/lib/libcuframes_static.a
filter-build:
name: ffmpeg filter patch (out-of-tree)
runs-on: ubuntu-22.04
container:
image: nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04
needs: cmake-build
steps:
- name: Bootstrap Node 20 + git (для actions/checkout)
run: |
set -e
export DEBIAN_FRONTEND=noninteractive
apt-get update
apt-get install -y --no-install-recommends curl git ca-certificates gnupg
# NodeSource setup может молча упасть на slow networks (особенно через VPN
# на u4-runner); retry + явная verification что Node >= 18 после install.
for i in 1 2 3; do
if curl -fsSL --retry 3 --retry-delay 5 --connect-timeout 30 \
https://deb.nodesource.com/setup_20.x | bash -; then
break
fi
echo "NodeSource setup attempt $i failed, retrying..."
sleep 10
done
apt-get install -y --no-install-recommends nodejs
NODE_VER=$(node --version)
echo "node: $NODE_VER"
# actions/checkout@v4 требует Node 20+ (ES2022 static blocks).
# Если NodeSource setup упал и установился Ubuntu's Node 12 — фейлим явно.
NODE_MAJOR=$(echo "$NODE_VER" | sed -E 's/^v([0-9]+).*/\1/')
if [ "$NODE_MAJOR" -lt 18 ]; then
echo "ERROR: Node $NODE_VER too old, NodeSource setup likely failed" >&2
exit 1
fi
- name: Install build deps
run: |
export DEBIAN_FRONTEND=noninteractive
apt-get install -y --no-install-recommends \
build-essential cmake ninja-build pkg-config nasm \
libssl-dev libx264-dev libx265-dev libnuma-dev zlib1g-dev \
wget patch
- name: Checkout
uses: actions/checkout@v4
- name: Build libcuframes (для linking в patched ffmpeg)
run: |
cmake -B build -S . -G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_TESTING=OFF -DBUILD_EXAMPLES=OFF -DBUILD_TOOLS=OFF
cmake --build build --parallel
cmake --install build --prefix /opt/cuframes
# Clone уже-patched FFmpeg fork с локального gitea (быстро + offline).
# Используем ${GITHUB_SERVER_URL} — runner подставит свой view на gitea:
# на R9-runner = http://192.168.88.23:3222, на u4-runner = http://10.8.0.6:3222 (VPN).
# Hardcoded https://git.goldix.org/... не работает на u4 — нет route к public IP.
- name: Clone patched FFmpeg fork (local gitea mirror)
run: |
git clone --depth 1 --branch n7.1-cuframes \
"${GITHUB_SERVER_URL}/gx/ffmpeg-patched.git" /src/ffmpeg
ls /src/ffmpeg/libavformat/cuframesdec.c
- name: Configure FFmpeg (minimal + libcuframes)
run: |
cd /src/ffmpeg
./configure \
--prefix=/opt/ffmpeg \
--enable-libcuframes \
--extra-cflags="-I/opt/cuframes/include -I/usr/local/cuda/include" \
--extra-ldflags="-L/opt/cuframes/lib -L/usr/local/cuda/lib64" \
--extra-libs="-lcudart -lpthread -lrt -lm" \
--disable-x86asm --disable-everything \
--enable-demuxer=cuframes,rawvideo \
--enable-decoder=rawvideo \
--enable-muxer=null,rawvideo \
--enable-protocol=file --enable-ffmpeg \
--disable-doc --disable-htmlpages --disable-manpages \
--disable-podpages --disable-txtpages
- name: Build FFmpeg
run: |
cd /src/ffmpeg
make -j$(nproc) ffmpeg
- name: Verify cuframes demuxer registered
run: |
export LD_LIBRARY_PATH=/opt/cuframes/lib
/src/ffmpeg/ffmpeg -hide_banner -formats | grep cuframes
/src/ffmpeg/ffmpeg -hide_banner -h demuxer=cuframes | head -10
+78
View File
@@ -0,0 +1,78 @@
name: release
# Триггер: push tag v* (e.g. v0.1.0, v0.2.0).
# Сборка: runtime Docker image + source tarball, прикладываем к gitea release.
on:
push:
tags:
- 'v*'
jobs:
docker-runtime:
name: build runtime Docker image
runs-on: ubuntu-22.04
container:
image: docker.gitea.com/runner-images:ubuntu-22.04
# docker socket нужен — gitea runner монтирует /var/run/docker.sock
volumes:
- /var/run/docker.sock:/var/run/docker.sock
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Tag from ref
id: tag
run: |
TAG="${GITHUB_REF#refs/tags/v}"
echo "version=$TAG" >> $GITHUB_OUTPUT
- name: Login to gitea registry
run: |
echo "${{ secrets.GITEA_TOKEN }}" | docker login git.goldix.org \
-u "${{ github.actor }}" --password-stdin
- name: Build runtime image
run: |
docker build -f docker/Dockerfile.runtime \
-t git.goldix.org/gx/cuframes:${{ steps.tag.outputs.version }} \
-t git.goldix.org/gx/cuframes:latest \
.
- name: Push
run: |
docker push git.goldix.org/gx/cuframes:${{ steps.tag.outputs.version }}
docker push git.goldix.org/gx/cuframes:latest
source-tarball:
name: build source tarball
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Tag from ref
id: tag
run: |
TAG="${GITHUB_REF#refs/tags/v}"
echo "version=$TAG" >> $GITHUB_OUTPUT
- name: Create tarball
run: |
VERSION="${{ steps.tag.outputs.version }}"
mkdir -p /tmp/release
git archive --format=tar.gz --prefix="cuframes-$VERSION/" \
-o "/tmp/release/cuframes-$VERSION.tar.gz" HEAD
ls -la /tmp/release/
# Готовый artifact — пользователь скачает с release page либо attached к release.
# Gitea release upload через API делается отдельным шагом (см. gitea/release-action
# либо curl); тут оставляем артефакт как build output для последующего ручного
# attach. Для полной автоматизации — добавить шаг upload через curl + GITEA_TOKEN.
- name: Upload tarball as artifact
uses: actions/upload-artifact@v3
with:
name: cuframes-${{ steps.tag.outputs.version }}-source
path: /tmp/release/cuframes-*.tar.gz
+21
View File
@@ -0,0 +1,21 @@
name: test-u4-runner
on:
workflow_dispatch:
push:
paths:
- '.gitea/workflows/test-u4-runner.yml'
jobs:
hello:
name: u4 runner smoke test
runs-on: u4
container:
image: ubuntu:24.04
steps:
- name: hostname + uname
run: |
echo "hostname: $(hostname)"
echo "uname: $(uname -a)"
echo "ip route: $(ip route | head -3)"
echo "test OK"
+92
View File
@@ -117,3 +117,95 @@ cd build && cmake -DBUILD_TESTING=ON .. && cmake --build . && ctest -R stress -
Production деplo замеры — см. интеграционные guides:
- [docs/integration.md](docs/integration.md) — cctv-processor C++ pipeline
- [filter/README.md](filter/README.md) — FFmpeg demuxer (Frigate setup)
---
## Real-world production deployment (2026-05-19, v0.2.0)
**Setup**: 4 Dahua IP-камеры (HEVC main 1920×1080 / 2688×1520, 25 fps) → 3
одновременных consumer'а на одном RTX 5090 хосте:
- **Frigate** detect (ONNX D-FINE-S, 640×480) + record (full-res H.265 mp4)
- **cctv-backend** custom C++ mosaic processor (composes 4×grid → RTSP output для TV)
### Before → after (measured production, идентичный workload)
| Метрика | Без cuframes | С cuframes v0.2 dual-input | Reduction |
|---|---:|---:|---:|
| **RTSP connections к камерам** | 12 (4 cam × 3 consumer) | **4** (publishers only) | **67%** |
| **NVDEC sessions** | ~8 (decode на каждый consumer) | **4** (publishers only) | **50%** |
| **Camera-side bandwidth** | ~34 Mbps (main+main+sub per cam) | **~16 Mbps** (main per cam) | **54%** |
| **PCIe D2H copies (consumer side)** | ~346 MB/s (decoded frames → host) | **~0** (zero-copy CUDA IPC) | **100%** |
| **Frigate ffmpeg с прямым RTSP** | 8 (detect+record × 4) | **0** (all через cuframes) | **100%** |
### Live nvidia-smi metrics в running system
```
GPU SM: 4-5% (compute: detector + cuframes consumers)
GPU NVDEC: 2-4% (без cuframes ожидаемо было 15-25%)
GPU NVENC: 0-1%
```
### VRAM breakdown (measured)
| Component | VRAM |
|---|---:|
| 4× cuframes publishers (3× FHD ring + 1× 2688×1520 для LPR) | **4.4 GB** |
| cctv-backend (composer + grid output) | 1.0 GB |
| frigate.embeddings_manager (face + LPR ONNX models) | 1.6 GB |
| frigate.detector:onnx (D-FINE-S COCO) | 0.6 GB |
| **Total cuframes-stack VRAM** | **~7.7 GB** |
Из них на сам cuframes accounting — только **4.4 GB** в publishers (ring buffers +
NVDEC decode buffers). Consumers (Frigate, cctv-backend) держат свои CUDA
contexts независимо.
### Network bandwidth (real tcpdump, 10-sec sample)
**31.5 Mbps** от camera subnet (4 cameras → R9), измерено через
`tcpdump -w cam-traffic.pcap` за 10 секунд.
Breakdown approximate:
- 4 publishers × main HEVC RTP/UDP: **~16 Mbps** (cuframes core)
- go2rtc on-demand streams (Frigate UI live preview, если открыт): **0-10 Mbps**
- ONVIF discovery, RTSP keepalives, NTP-from-cameras: **~1-2 Mbps**
Без cuframes тот же setup (cctv-backend + Frigate detect + Frigate record × 4
camera) дал бы **~45-50 Mbps** (главное: record path забирал отдельный
main stream от каждой camera).
### Camera-side benefits
Dahua/Hikvision камеры обычно cap'нуты на 4-5 одновременных RTSP streams.
До cuframes setup (4 cam × 3 RTSP) делал каждую camera на **60-75% capacity**
её RTSP server'а. После — **20-25%**, headroom на 2-3 дополнительных
consumer'а без замены оборудования.
### Что **сохранено** (важно)
- **Качество записи**: record path через `cuframes_packets://` это **passthrough**
(`-c:v copy`), bit-exact original encoded stream от камеры. Frigate пишет mp4
с full-resolution оригинала, без re-encode.
- **Latency**: <2 ms publisher → consumer (cuframes IPC) vs ~50-80 ms RTSP setup
latency для каждого нового consumer.
- **Backward compatibility**: v0.2 publishers принимают v1 subscribers
(frames-only), rolling upgrade.
### Hardware-agnostic projection (для другого setup)
| If you have | Expected reduction |
|---|---|
| 16 cameras × 2 consumers | 32 → 16 NVDEC (50%), 32 → 16 RTSP (50%) |
| 8 cameras × 3 consumers | 24 → 8 NVDEC (67%), 24 → 8 RTSP (67%) |
| 4 cameras × 4 consumers (multi-AI pipeline) | 16 → 4 NVDEC (75%), 16 → 4 RTSP (75%) |
Reduction масштабируется **линейно** с N (consumers per camera). v0.1 (frames
only) сэкономит NVDEC; v0.2 (frames + packets) **дополнительно** сэкономит
RTSP connections для record/mux consumers.
### Что **НЕ** сэкономлено (честно)
- **Disk space**: запись остаётся full-resolution H.265 mp4. Cuframes не сжимает.
- **Detector inference latency**: ONNX/TensorRT detector работает на decoded
frames независимо от source. Cuframes только меняет где decode произошёл.
- **Camera RTSP server CPU**: сама камера всё равно encode'ит видео. Cuframes
reduces **consumer-side** load, не producer-side.
+45
View File
@@ -5,6 +5,51 @@
Формат основан на [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
проект следует [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [0.2.0] — 2026-05-19
Encoded packet ring — параллельный канал для record/mux consumer'ов
без второго RTSP-подключения к камере.
См. issue [#2](https://git.goldix.org/gx/cuframes/issues/2),
PRs [#4](https://git.goldix.org/gx/cuframes/pulls/4) (cuframes) +
[gx/ffmpeg-patched#1](https://git.goldix.org/gx/ffmpeg-patched/pulls/1)
(FFmpeg demuxer).
### Added
- **Encoded packet ring** — параллельный ring для H.264/H.265 NAL units
(отдельный SHM `/dev/shm/cuframes-<key>-packets`, variable-length byte
buffer + slot index, seqlock-style read для защиты от overrun).
- **Wire protocol v2** (`proto_version = 2` в SHM header). Backward-compat:
v2 publishers принимают v1 subscribers (frames-only).
- **Public C API** (`include/cuframes/cuframes.h`):
- `cuframes_publisher_enable_packets(opts)` — активирует ring
- `cuframes_publisher_set_codec_extradata(data, size)` — SPS/PPS
- `cuframes_publisher_publish_packet(data, size, pts, dts, flags)`
- `cuframes_subscriber_enable_packets()` + `_next_packet()` + accessors
- `cuframes_subscriber_get_codec_params(codec_id, extradata, size)`
- **`cuframes::Publisher`** (C++ RAII): `enable_packets`, `set_codec_extradata`,
`publish_packet` методы.
- **`cuframes-rtsp-source`**: новый CLI flag `--enable-packet-ring`.
Дублирует `AVPacket` в encoded ring до передачи декодеру.
- **FFmpeg demuxer `cuframes_packets://<key>`** (отдельная ветка
[gx/ffmpeg-patched PR #1](https://git.goldix.org/gx/ffmpeg-patched/pulls/1)).
Companion к `cuframes://`. Use case: Frigate `record` role без
второго RTSP к камере.
- **4 новых error codes**: `PACKET_OVERSIZED`, `NO_PACKET_RING`,
`NO_CODEC_PARAMS`, `PACKET_OVERRUN`.
- **Stress test** `libcuframes/tests/test_packet_ring.c`: 2 scenarios —
normal flow (1 pub × 1 sub × 2000 packets, integrity check) +
slow consumer (must hit OVERRUN + library auto-resync на keyframe).
- **Protocol spec §10** в `docs/protocol.md` (397 строк): byte-exact
layout, seqlock semantics, late-subscriber GOP-aligned start.
### Limitations (документировано)
- Sub-stream selection отложено в v0.3 (`<key>-substream-<N>` naming).
- Audio packets — v0.3 (тот же ring layout, codec_id = audio).
- Codec change mid-stream — требует publisher destroy+recreate.
## [0.1.0] — 2026-05-17
Первый функциональный release с production deployment.
+6 -2
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.20)
project(cuframes
VERSION 0.1.0
DESCRIPTION "Zero-copy frame sharing via CUDA IPC"
VERSION 0.4.0
DESCRIPTION "Zero-copy frame sharing via CUDA VMM + POSIX FD"
LANGUAGES C CXX CUDA
)
@@ -39,3 +39,7 @@ endif()
if(BUILD_TOOLS)
add_subdirectory(tools/cuframes-rtsp-source)
endif()
if(BUILD_PYTHON_BINDINGS)
add_subdirectory(python)
endif()
+8 -3
View File
@@ -1,10 +1,15 @@
# cuframes
[![build](https://git.goldix.org/gx/cuframes/actions/workflows/build.yml/badge.svg?branch=main)](https://git.goldix.org/gx/cuframes/actions?workflow=build.yml)
[![release](https://img.shields.io/badge/release-v0.1.0-blue)](https://git.goldix.org/gx/cuframes/releases/tag/v0.1.0)
[![license](https://img.shields.io/badge/license-LGPL--2.1+-green)](LICENSE)
Zero-copy sharing декодированных видеокадров между процессами через CUDA IPC.
**Статус:** v0.1 — libcuframes готов, cuframes-rtsp-source готов, e2e-pipeline
протестирован (4×subscriber × 2000 frames, 0 torn). FFmpeg filter — v0.2.
**Лицензия:** LGPL-2.1+
**Статус:** v0.1.0 released — production-deployed на multi-camera CCTV-стeке
(Frigate + custom C++ processor, оба используют один publisher на одном NVDEC).
См. [BENCHMARKS.md](BENCHMARKS.md) для measurements, [ROADMAP.md](ROADMAP.md)
для v0.2 plans.
## Минимальные требования
+30
View File
@@ -59,6 +59,36 @@ ETA: 1-2 недели focused работы.
| Frigate plugin POC (Python side, не FFmpeg) | Альтернативный путь для users которые не хотят патчить FFmpeg |
| Docker images в public registry | Snapshot CI-built tarballs + multi-arch |
## Future ideas 💡 (не запланированы, без ETA)
Идеи которые не привязаны к конкретной версии и ждут планирования.
### `gst-cuframes-src` — GStreamer source-element
Аналог FFmpeg-демуксера для GStreamer-стэка. Один publisher cuframes-side → potreбители-pipeline'ы в GStreamer (DeepStream, обычный GStreamer-приложения).
| Зачем | Что |
|---|---|
| NVIDIA DeepStream — это GStreamer-native, FFmpeg-демуксер там не работает | `gst-cuframes-src` как `GstBaseSrc`-derived element, выдаёт `GstBuffer` с `GstCudaMemory` (NVMM в Jetson вариант) |
| GStreamer-приложения (обычный software) | Drop-in source для любой GStreamer pipeline |
| GStreamer plugin registry | `gst-inspect-1.0 cuframessrc` discoverable |
Open questions: какой memory-type — `memory:CUDAMemory` (mainline) vs `memory:NVMM` (NVIDIA DeepStream-specific). Возможно два варианта/build flags.
### `vf_cuda_grid` — **выделен в отдельный продукт `gx/vf-cuda-grid`** ([repo](https://git.goldix.org/gx/vf-cuda-grid))
FFmpeg filter для GPU-native video grid composition + control-plane sidecar
(ZeroMQ/MQTT/HTTP/HA Discovery). Дизайн зафиксирован, см.
[`gx/vf-cuda-grid` docs/design.md](https://git.goldix.org/gx/vf-cuda-grid/src/branch/main/docs/design.md)
и [epic issue #1](https://git.goldix.org/gx/vf-cuda-grid/issues/1).
Cuframes остаётся frame source provider для vf-cuda-grid в нашей экосистеме
(но vf-cuda-grid работает и с любым другим CUDA frame source — стандартный FFmpeg).
Закрывает [`gx/cctv#22`](https://git.goldix.org/gx/cctv/issues/22) Phase 4
(end-to-end GPU pipeline для cctv-processor mosaic composer) после Phase 4 vf-cuda-grid +
миграция cctv-processor GridComposer → vf_cuda_grid filter.
## v1.0 — Stable ABI 📋
- Стабильный wire-protocol (minor versions add fields в reserved space)
+5 -3
View File
@@ -16,7 +16,8 @@
# /usr/local/bin/cuframes-rtsp-source --rtsp ... --key ...
# ─── Build stage ─────────────────────────────────────────────────────────
FROM nvidia/cuda:13.0.3-cudnn-devel-ubuntu24.04 AS build
# CUDA 12.4 — matching ffmpeg-vf-cuda-grid base + Frigate stable-tensorrt
FROM nvidia/cuda:12.4.1-devel-ubuntu22.04 AS build
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -36,12 +37,13 @@ RUN cmake -B build -S . -G Ninja \
&& cmake --build build --parallel
# ─── Runtime stage ────────────────────────────────────────────────────────
FROM nvidia/cuda:13.0.3-cudnn-runtime-ubuntu24.04 AS runtime
FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04 AS runtime
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
libavcodec60 libavformat60 libavutil58 \
libavcodec58 libavformat58 libavutil56 \
ca-certificates \
mosquitto-clients \
&& rm -rf /var/lib/apt/lists/*
# libcuframes.so → /usr/local/lib (стандартный путь для ldconfig)
+81
View File
@@ -423,3 +423,84 @@ mosaic + RTSP-server. После v1 cuframes:
3. После Phase 0 — review результатов, корректировка дизайна (если CUDA IPC
повёл себя не как ожидали)
4. Phase 1+ по плану
---
# Appendix A — Production deployment notes (post-v0.1.0)
Реальные наблюдения после первого production deployment (Frigate + cctv-processor
на RTX 5090, 24h+ uptime). Обновляется по мере накопления опыта.
## Что подтвердилось из изначального дизайна
- **CUDA IPC handshake через cudaIpcEventHandle_t работает стабильно** — нет
ни одного torn frame за 24+ часов на 2 consumer'ах.
- **EXTERNAL ownership** (publisher передаёт свои pre-allocated CUDA pointers)
необходим для FFmpeg-based publisher — иначе нужен extra cudaMemcpy из FFmpeg's
hwframe pool в library-managed pool.
- **Unix socket handshake** ОК — простой, debug'абельный (`socat` для inspect).
- **POSIX shm для header + atomic seq counters** — race-free на reader side.
## Что пришлось доработать в v0.1.0 vs initial design
- **CMake install rules** изначально не предусмотрены. Downstream проекты
делали `cmake --install` → пустой prefix. Fix: `install(TARGETS ...)` +
`install(DIRECTORY include/cuframes ...)`. Лессон — install rules должны
быть в day 1.
- **Variable HINTS в find_library**: пользователи делают install в разные
prefix'ы. HINTS для downstream `find_library(cuframes)` должны включать
`$PREFIX/lib`, `$PREFIX/lib64`, и `build-dir/libcuframes/` для local-dev.
## Что не учли в дизайне (открытые grабли — см. troubleshooting.md)
### Cross-container CUDA IPC требует **shared pid + ipc namespace**
`cudaIpcOpenEventHandle` validates IPC peer через `/proc/<pid>/...`. Если
consumer container не в same PID namespace что publisher — fail с
`invalid device context`.
Это **incompatible** с s6-overlay-based containers (linuxserver.io stack,
Frigate), требующими PID 1 для self. Workaround: только `ipc:` shared,
accept race window (works на Frigate в практике потому что подключается
первым после publisher restart). **Real fix planned v0.2**: socket-based
context validation вместо `/proc` reliance.
### Publisher-side resize нужен для consumers без cuda-llvm
Большинство downstream FFmpeg builds — без `--enable-cuda-llvm` (на платформах
с glibc < 2.38 эта опция не собирается, нужен `stdbit.h`). Без cuda-llvm нет
`scale_cuda` filter. Consumer вынужден CPU-resize либо отключать hwaccel.
**Fix planned v0.2**: publisher принимает `--scale=WxH` и делает GPU resize
до publish. Consumer получает уже scaled frames, scale_cuda не нужен.
### Encoded packet sharing — отсутствует в v0.1
cuframes v0.1 раздаёт **только decoded** NV12. Для `record` use case
(`-c:v copy` mux без decode) consumer всё ещё открывает свой RTSP — лимит
камеры на concurrent streams (4-5 у Dahua) hit'ится.
**v0.2 spec**: parallel encoded-packets ring + `cuframes_packets://`
demuxer. См. [issue #2](https://git.goldix.org/gx/cuframes/issues/2).
## Production setup (gold path)
```
┌─► Frigate (FFmpeg cuframes:// demuxer) → detect
Camera RTSP ─► publisher ──┤
(1× NVDEC) └─► cctv-processor (CuframesSource C++ API) → motion+RTSP-encode→TV
```
| Метрика | Without cuframes (baseline) | С cuframes v0.1 |
|---|---|---|
| NVDEC operations на parking-камеру | 2 (Frigate detect + cctv detect) | **1** (publisher) |
| VRAM extra cost | 0 (каждый своё) | ~3 MB (ring 6×460KB sub-stream) |
| RTSP camera load | 2 streams | **1** stream |
| Uptime (verified) | n/a | 24h+ без drops |
## См. также
- [docs/troubleshooting.md](troubleshooting.md) — конкретные грабли + fixes
- [BENCHMARKS.md](../BENCHMARKS.md) — измерения
- [docs/integrations/frigate.md](integrations/frigate.md) — guide для Frigate
- [ROADMAP.md](../ROADMAP.md) — v0.2/v0.3/v1.0
+38 -234
View File
@@ -1,11 +1,20 @@
# Integration guide
Этот guide описывает, как использовать cuframes для устранения дублирующего
GPU-декодирования между несколькими consumer'ами одного RTSP-потока.
Хочешь подключить cuframes к своему проекту? Выбери guide по типу integration'а:
## Готовые reference guides
| Тип integration'а | Guide | Reference deployment |
|---|---|---|
| **Frigate NVR** (через FFmpeg `cuframes://` demuxer) | [integrations/frigate.md](integrations/frigate.md) | Production: Frigate 0.17.1 + RTX 5090 + Dahua HEVC |
| **C++ project** (через `CuframesSource` pattern) | [integrations/cctv-cpp.md](integrations/cctv-cpp.md) | Production: [gx/cctv](https://git.goldix.org/gx/cctv) C++17 processor |
| **Python AI/ML pipeline** (через ctypes wrapper) | [examples/python-consumer/](../examples/python-consumer/) | Skeleton ready; v0.3 даст native bindings |
| **FFmpeg-based custom tool** (своя сборка ffmpeg) | [filter/README.md](../filter/README.md) | Out-of-tree patch + build instructions |
## Целевой сценарий (motivation)
В типичной CCTV-системе один и тот же RTSP-stream декодируется несколько раз:
В типичной CCTV / video-analytics системе один и тот же RTSP-поток
декодируется **несколько раз**:
```
Камера ──► RTSP ──► Frigate (decode #1: detection + recording)
@@ -13,13 +22,14 @@ GPU-декодирования между несколькими consumer'ами
─► AI-скрипт (decode #3: классификация / OCR)
```
На 16 камер × 25 fps × 3 consumer'а = 1200 NVDEC-операций/сек. RTX 5090 имеет
~3 NVDEC-движка, но шина PCIe и memory bandwidth становятся узким местом.
На 16 камер × 25 fps × 3 consumer'а = **1200 NVDEC operations/sec**. RTX 5090
имеет ~3 NVDEC-движка с capacity ~50 FHD25 streams → загрузка близка к лимиту,
плюс tax на PCIe bandwidth и memory.
С cuframes:
```
Камера ──► cuframes-rtsp-source ──► CUDA frame в /dev/shm + cudaIpcEvent
Камера ──► cuframes-rtsp-source ──► CUDA frame в VRAM + IPC handles
├──► Frigate (zero-copy)
├──► mosaic-сервер (zero-copy)
@@ -27,242 +37,36 @@ GPU-декодирования между несколькими consumer'ами
```
Decode выполняется **один раз** на источник, потребители получают тот же CUDA
device pointer без копий.
device pointer без копий. **3× меньше NVDEC operations** на том же setup'е.
## Текущие limitations v0.1
## Текущие ограничения (v0.1)
- **Frigate** (по состоянию на 0.17) **не имеет** plugin-точки для приёма
готовых CUDA-frames. Чтобы убрать Frigate decode полностью, нужен:
- либо FFmpeg-filter `vf_cuda_ipc_input` (planned для cuframes v0.2 — требует
patch FFmpeg upstream и пересборку Frigate's bundled ffmpeg),
- либо Frigate-plugin (требует upstream работы с командой Frigate).
- В v0.1 практическое улучшение: **исключить decode для всех custom consumer'ов
кроме Frigate** (то есть cctv-processor, AI-скрипты — на cuframes; Frigate
остаётся как есть, со своим decode).
- **Decoded frame sharing only** (не encoded). Для `record` path в Frigate
(mux без decode) consumer всё ещё открывает свой RTSP — это решит **v0.2
encoded packet sharing** (см. [issue #2](https://git.goldix.org/gx/cuframes/issues/2)).
Это уже даёт значительную экономию: было 1×Frigate + N×consumer decode'ов,
стало 1×Frigate + 1×cuframes-rtsp-source (один на все consumer'ы).
- **NV12 frame format only**. Other formats (YUV420P, RGB) — v0.2.
## Сценарий 1: cuframes-rtsp-source + cctv-processor (FRIGATE остаётся)
- **GPU → CPU copy** в FFmpeg demuxer'е (`cudaMemcpy2DAsync`). Zero-copy через
`AVHWFramesContext` — v0.2.
### docker-compose.yml
- **Cross-container CUDA IPC** требует shared `ipc + pid` namespace. Если
consumer использует s6-overlay (как Frigate) — pid не shareable, нужен
workaround (см. [integrations/frigate.md](integrations/frigate.md)
troubleshooting).
```yaml
services:
# Один источник на камеру — публикует декодированный поток через cuframes IPC
cuframes-cam-parking:
image: gx/cuframes-rtsp-source:0.1
restart: unless-stopped
runtime: nvidia
environment:
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
# CRITICAL: --ipc=shareable для cross-container CUDA IPC
ipc: shareable
shm_size: 1g
volumes:
- cuframes_sock:/run/cuframes
command:
- --rtsp=rtsp://admin:${CAM_PASS}@192.168.88.98:554/cam/realmonitor?channel=1&subtype=0
- --key=cam-parking
- --ring=6
- --realtime # не нужен для RTSP (real-time источник), оставлен для file://
- **Только Linux + NVIDIA GPU** compute capability ≥ 7.5 (Turing+).
# Frigate (как и был — со своим decode на main+sub streams)
frigate:
image: ghcr.io/blakeblackshear/frigate:stable-tensorrt
# ... как обычно
## Production reference deployments
# cctv-processor — подписывается на cuframes (без отдельного RTSP decode)
cctv-backend:
image: gx/cctv-processor:cuda
restart: unless-stopped
runtime: nvidia
# CRITICAL: shared IPC + PID namespace с publisher'ом (см. ниже)
ipc: container:cuframes-cam-parking
pid: container:cuframes-cam-parking
volumes:
- cuframes_sock:/run/cuframes:ro
environment:
# cuframes-keys для backend'а:
CCTV_SOURCES: cuframes:cam-parking,cuframes:cam-front-gate,...
| Setup | Версия | Где смотреть |
|---|---|---|
| 1 publisher (1× NVDEC) → Frigate (detect) + cctv-backend (motion+grid→RTSP→TV) | v0.1.0 | [BENCHMARKS.md](../BENCHMARKS.md), [integrations/frigate.md](integrations/frigate.md) |
volumes:
cuframes_sock:
```
## Roadmap для v0.2+
**Важно — оба флага обязательны** для cross-container CUDA IPC:
Полный roadmap — [ROADMAP.md](../ROADMAP.md). Highlights:
| Флаг | Зачем |
|---|---|
| `ipc: container:<publisher>` | shared `/dev/shm` (нужен для `shm_open` под header/sockets) |
| `pid: container:<publisher>` | CUDA driver валидирует IPC peer через `/proc/<pid>/...`; без этого `cudaIpcOpenEventHandle` падает с `invalid device context` |
Альтернативы:
- Запускать consumer внутри того же container'а через `docker exec` (наследует все namespaces) — удобно для отладки.
- `--ipc=host --pid=host` — убирает namespacing вообще, но ослабляет изоляцию (не рекомендуется в production).
### Изменения в cctv-processor
Нужно добавить новый Source-тип (рядом с RtspSource) — `CuframesSource`:
```cpp
// cpp/apps/cctv-processor/src/sources/cuframes_source.hpp
#include <cuframes/cuframes.hpp>
class CuframesSource : public IVideoSource {
public:
CuframesSource(const std::string &key) : key_(key) {
cuframes::SubscriberOptions opt;
opt.key = key;
opt.consumer_name = "cctv-processor";
opt.mode = CUFRAMES_MODE_NEWEST_ONLY;
sub_ = std::make_unique<cuframes::Subscriber>(opt);
cudaStreamCreate(&stream_);
}
// Вызывается processing-loop'ом
std::optional<GpuFrame> nextFrame() override {
auto f = sub_->next(stream_, 100); // 100ms timeout
if (!f) return std::nullopt;
// cudaStreamWaitEvent уже сделан внутри next() — frame готов на stream_
return GpuFrame{
.cuda_ptr = f->cuda_ptr(),
.width = f->width(),
.height = f->height(),
.pitch_y = f->pitch_y(),
.pitch_uv = f->pitch_uv(),
.seq = f->seq(),
.pts_ns = f->pts_ns(),
.stream = stream_,
._release = std::move(f), // RAII release при destroy
};
}
private:
std::string key_;
std::unique_ptr<cuframes::Subscriber> sub_;
cudaStream_t stream_;
};
```
Конфиг `cameras.json` — добавить альтернативный source-тип:
```jsonc
{
"cameras": [
{
"id": "parking",
"source_type": "cuframes", // вместо "rtsp"
"cuframes_key": "cam-parking",
// rtsp_url больше не нужен — он используется cuframes-rtsp-source'ом
}
]
}
```
## Сценарий 2: AI-скрипт на Python (subscriber)
Python-bindings — в Phase 3 cuframes. Сейчас простой workaround через
ctypes:
```python
import ctypes
lib = ctypes.CDLL("libcuframes.so")
# ... wrap нужные функции — см. include/cuframes/cuframes.h
```
Или: writer simple C-обёртку, которая принимает callback и публикует
данные через ZMQ / shared memory в python-process.
## Сценарий 3: Замена Frigate decode (v0.2+)
Целевой сценарий — Frigate тоже подписан на cuframes. Реализуется через
один из двух путей:
### Путь A: FFmpeg filter
Добавить out-of-tree filter `vf_cuda_ipc_input` который читает кадр из
cuframes ring и эмитит AVFrame в pipeline. Frigate использует ffmpeg для
RTSP/decode — заменяем "RTSP→decode→detect" на
"cuframes_ipc_input→detect" (без decode'а вообще).
Требования:
- Patch ffmpeg sources (libavfilter/vf_cuda_ipc_input.c + Makefile)
- Сборка кастомного Frigate-образа с patched ffmpeg
- Тестирование на совместимость с Frigate's pipeline assumptions
### Путь B: Frigate plugin
Engage с upstream Frigate чтобы добавить custom Source-type ("cuframes://").
Это требует Python-API изменений в Frigate's source layer.
## Verification checklist
После настройки убедитесь:
```bash
# 1. Publisher запущен и socket существует
ls -la /run/cuframes/cam-parking.sock
ls -la /dev/shm/cuframes-cam-parking
# 2. Контейнеры в одном IPC и PID namespace
docker inspect cuframes-cam-parking cctv-backend \
-f '{{.Name}} ipc={{.HostConfig.IpcMode}} pid={{.HostConfig.PidMode}}'
# Publisher: ipc=shareable pid=(default)
# Consumer: ipc=container:cuframes-cam-parking pid=container:cuframes-cam-parking
# 3. Subscriber connect успешен
docker exec cctv-backend /usr/local/bin/sub_count --key cam-parking --max-frames 10
# Ожидаем:
# [sub_count] connected to 'cuframes-cam-parking'
# [sub_count] received=10 gaps=0 elapsed=0.4s avg_fps=25
# 4. NVDEC utilization — должно быть N decodes, а не N*M
nvidia-smi dmon -s u
# Колонка %dec должна показать decode-нагрузку одного instance на камеру
```
## Troubleshooting
### `Subscriber::create: timeout`
Subscriber не нашёл publisher. Причины:
- Publisher не запущен или crashed — проверь `docker logs cuframes-cam-parking`
- Socket-файл не volumes'нут в consumer-контейнер — добавь `volumes:
- cuframes_sock:/run/cuframes:ro` в consumer'е
- IPC namespace не совпадает — см. checklist пункт 2
### `cudaIpcOpenEventHandle: invalid device context`
Проявляется в **отдельном** consumer-container'е после успешного handshake (socket
открыт, header валиден, но open event handle не проходит).
Причина: CUDA driver валидирует sender'а IPC peer'а через `/proc`. Если PID
namespace не совпадает, sender невидим — context считается невалидным.
Fix: добавить `pid: container:<publisher>` в consumer's compose service (рядом
с `ipc: container:<publisher>`). Проверено на CUDA 13.0 + driver 555+.
### `cudaIpcOpenMemHandle returned 'invalid device pointer'`
- Контейнеры в РАЗНЫХ ipc namespace — должны быть в одном (через
`ipc: container:<publisher>` или общий `ipc: shareable`)
- Subscriber работает на другом CUDA device — `--cuda-device` должен совпадать
у publisher и subscriber (одно и то же физическое GPU)
### Высокая latency (>50ms tail)
- Subscriber slow — frames копятся в ring, по политике DROP_OLDEST они
пропускаются. Используй `CUFRAMES_MODE_NEWEST_ONLY` (default) — это нормально
для real-time системы.
- При STRICT_ORDER + STRICT_WAIT — slow consumer блокирует publisher. Не
рекомендуется для CCTV.
### Frigate показывает чёрный экран после интеграции
- Frigate не подключён к cuframes (v0.1 — это not yet supported). В v0.1
Frigate должен оставаться на своём RTSP decode (см. Сценарий 1).
## Roadmap
- **v0.1** (текущая): standalone publisher/subscriber, C/C++ API, examples.
- **v0.2**: FFmpeg filter `vf_cuda_ipc_input` (out-of-tree), Python bindings.
- **v0.3**: NVENC-bridge для re-encode подписчиков, Frigate plugin
proof-of-concept.
- **v1.0**: stable ABI, multi-GPU, documented Frigate integration.
- **v0.2**: encoded packet sharing (Frigate record без второго RTSP), FFmpeg upstream PR, publisher-side resize для устранения scale_cuda dependency
- **v0.3**: pybind11 Python bindings, Jetson/arm64 support
- **v1.0**: stable ABI, multi-GPU, env-based credentials
+309
View File
@@ -0,0 +1,309 @@
# C++ project integration (cctv-processor pattern)
Reference guide на основе реального production deployment
([gx/cctv](https://git.goldix.org/gx/cctv) — C++17 video processor).
## Use case
Custom video pipeline (motion detection, mosaic compose, encode-out, snapshots),
получает кадры с N камер и выполняет per-frame processing. Без cuframes:
один RTSP+NVDEC на каждую камеру **внутри** processor + дублирующий decode
если Frigate/AI script тоже подключены к той же камере.
С cuframes: processor подписывается на published frames, **никакого RTSP / NVDEC**
у него — все консьюмеры используют один decode от publisher'а.
## Архитектурный паттерн
Выделить **interface** `IFrameSource` чтобы pipeline не зависел от конкретного
источника (RTSP vs cuframes vs тестовый file).
```cpp
// include/sources/IFrameSource.h
namespace cctv::sources {
enum class ConnectionState {
DISCONNECTED, CONNECTING, CONNECTED, RECONNECTING, ERROR
};
struct StreamInfo {
int width = 0;
int height = 0;
double fps = 0.0;
std::string codec_name;
int64_t bitrate = 0;
};
class IFrameSource {
public:
using FrameCallback = std::function<void(const cv::Mat& frame, int64_t ts_ms)>;
using StateCallback = std::function<void(ConnectionState, const std::string&)>;
virtual ~IFrameSource() = default;
virtual bool connect(const std::string& url) = 0;
virtual void disconnect() = 0;
virtual bool isConnected() const = 0;
virtual void setFrameCallback(FrameCallback) = 0;
virtual void setStateCallback(StateCallback) = 0;
virtual void setReconnectEnabled(bool) = 0;
virtual StreamInfo getStreamInfo() const = 0;
virtual ConnectionState getState() const = 0;
virtual std::string getLastError() const = 0;
virtual uint64_t getFramesReceived() const = 0;
virtual uint64_t getFramesDropped() const = 0;
virtual double getCurrentFPS() const = 0;
};
} // namespace cctv::sources
```
`RTSPClient` (legacy) и `CuframesSource` оба implement `IFrameSource`. Pipeline
работает с `unique_ptr<IFrameSource>` — code не знает, RTSP это или cuframes.
## CuframesSource — реализация
```cpp
// include/sources/CuframesSource.h
#include "sources/IFrameSource.h"
// Forward-declare — не утекают в header
struct cuframes_subscriber;
typedef struct cuframes_subscriber cuframes_subscriber_t;
namespace cctv::sources {
class CuframesSource : public IFrameSource {
public:
CuframesSource();
~CuframesSource() override;
// IFrameSource: URL для cuframes — это просто `key` (либо "cuframes://<key>")
bool connect(const std::string& url) override;
void disconnect() override;
// ... остальные методы (см. полный файл в gx/cctv repo)
void setCudaDevice(int device);
void setReconnectInterval(int seconds);
private:
void workerThread();
bool openSubscriber();
void closeSubscriber();
std::string m_key;
int m_cudaDevice = 0;
cuframes_subscriber_t* m_sub = nullptr;
void* m_cudaStream = nullptr; // cudaStream_t, opaque
void* m_hostBuffer = nullptr; // pinned host buffer для NV12
size_t m_hostBufferSize = 0;
std::thread m_thread;
std::atomic<bool> m_shouldStop{false};
// ... callbacks, state, stats
};
} // namespace cctv::sources
```
### Worker thread (core)
```cpp
void CuframesSource::workerThread() {
while (!m_shouldStop.load()) {
if (!m_sub) {
if (!openSubscriber()) {
changeState(ConnectionState::RECONNECTING, m_lastError);
if (!m_reconnectEnabled) return;
sleep_for(seconds(m_reconnectInterval));
continue;
}
changeState(ConnectionState::CONNECTED, "");
}
cuframes_frame_t* frame = nullptr;
int rc = cuframes_subscriber_next(m_sub, m_cudaStream, &frame, 200);
if (rc == CUFRAMES_ERR_TIMEOUT) continue;
if (rc == CUFRAMES_ERR_DISCONNECTED) {
closeSubscriber();
changeState(ConnectionState::RECONNECTING, "publisher disconnected");
continue;
}
if (rc != CUFRAMES_OK || !frame) {
LOG_ERROR("cuframes next: " + std::string(cuframes_strerror(rc)));
closeSubscriber();
continue;
}
// Frame metadata
int32_t w, h;
cuframes_frame_size(frame, &w, &h);
const int32_t pitch_y = cuframes_frame_pitch_y(frame);
const int32_t pitch_uv = cuframes_frame_pitch_uv(frame);
const int64_t pts_ns = cuframes_frame_pts_ns(frame);
// Ensure host buffer big enough
const size_t need = (size_t)w * h * 3 / 2; // NV12 packed
if (need > m_hostBufferSize) {
cudaFreeHost(m_hostBuffer);
cudaMallocHost(&m_hostBuffer, need);
m_hostBufferSize = need;
}
// Copy GPU NV12 → host NV12 (Y plane + UV plane)
uint8_t* cu = (uint8_t*)cuframes_frame_cuda_ptr(frame);
cudaMemcpy2DAsync(m_hostBuffer, w, cu, pitch_y,
w, h, cudaMemcpyDeviceToHost, m_cudaStream);
cudaMemcpy2DAsync((uint8_t*)m_hostBuffer + (size_t)w*h, w,
cu + (size_t)pitch_y*h, pitch_uv,
w, h/2, cudaMemcpyDeviceToHost, m_cudaStream);
cudaStreamSynchronize(m_cudaStream);
// Release frame BEFORE downstream processing — publisher может переиспользовать slot
cuframes_subscriber_release(m_sub, frame);
// NV12 → BGR (CPU) — downstream pipeline ожидает cv::Mat BGR
cv::Mat nv12(h * 3 / 2, w, CV_8UC1, m_hostBuffer);
cv::Mat bgr;
cv::cvtColor(nv12, bgr, cv::COLOR_YUV2BGR_NV12);
// Доставка через callback
if (m_frameCallback) m_frameCallback(bgr, pts_ns / 1000000);
}
closeSubscriber();
}
```
`cudaMemcpy → CPU → cv::cvtColor` это v0.1 path. **Zero-copy** через
`AVHWFramesContext` / OpenCV cv::cuda::GpuMat — planned v0.2.
## Factory pattern (per-camera)
```cpp
// В StreamProcessor::initializeComponents()
for (const auto& camera : cameras) {
if (!camera.enabled) continue;
std::unique_ptr<sources::IFrameSource> source;
if (camera.source_type == "cuframes") {
source = std::make_unique<sources::CuframesSource>();
} else {
source = std::make_unique<rtsp::RTSPClient>(); // legacy RTSP
}
source->setFrameCallback([this, id = camera.id](const cv::Mat& frame, int64_t ts) {
m_videoProcessor->processFrame(id, frame);
});
source->setStateCallback([this, id = camera.id](auto state, const std::string& msg) {
// logging, alerting, watchdog
});
source->setReconnectEnabled(true);
m_frameSources[camera.id] = std::move(source);
}
```
В `start()` — отдельный цикл:
```cpp
for (const auto& camera : cameras) {
if (!camera.enabled) continue;
auto& src = m_frameSources[camera.id];
const std::string url = (camera.source_type == "cuframes")
? camera.cuframes_key
: camera.rtsp_url;
src->connect(url);
}
```
## CMake integration
```cmake
# cmake/Dependencies.cmake
if(ENABLE_CUDA AND CUDA_AVAILABLE)
find_path(CUFRAMES_INCLUDE_DIR cuframes/cuframes.h
HINTS ${CUFRAMES_ROOT}/include /usr/local/include /usr/include
)
find_library(CUFRAMES_LIBRARY cuframes
HINTS ${CUFRAMES_ROOT}/lib ${CUFRAMES_ROOT}/lib64
/usr/local/lib /usr/lib
)
if(CUFRAMES_INCLUDE_DIR AND CUFRAMES_LIBRARY)
set(CUFRAMES_FOUND TRUE)
find_package(CUDAToolkit REQUIRED)
message(STATUS "cuframes: FOUND (${CUFRAMES_LIBRARY})")
else()
message(STATUS "cuframes: NOT FOUND (camera source_type=cuframes недоступен)")
endif()
endif()
```
```cmake
# apps/your-processor/CMakeLists.txt
if(CUFRAMES_FOUND)
target_include_directories(your-processor PRIVATE ${CUFRAMES_INCLUDE_DIR})
target_link_libraries(your-processor PRIVATE ${CUFRAMES_LIBRARY} CUDA::cudart)
target_compile_definitions(your-processor PRIVATE CCTV_HAVE_CUFRAMES=1)
endif()
```
`CuframesSource.cpp` оборачивается в `#ifdef CCTV_HAVE_CUFRAMES` — без cuframes
в системе фабрика возвращает error при `source_type == "cuframes"`, остальное
компилируется как обычно.
## Config
`cameras.json` extension:
```json
{
"cameras": [
{
"id": 1,
"name": "Парковка через cuframes",
"source_type": "cuframes",
"cuframes_key": "cam-parking",
"rtsp_url": "",
"enabled": true,
"motion_detection": { "enabled": false, ... }
},
{
"id": 2,
"name": "Камера на RTSP",
"rtsp_url": "rtsp://admin:pw@cam-ip:554/stream",
"enabled": true
}
]
}
```
## Runtime requirements
Consumer container/process должен:
1. Иметь доступ к `/run/cuframes` (volume mount от publisher'а).
2. Быть в **same** IPC namespace (для `/dev/shm` shared) — `ipc: container:<publisher>`.
3. Быть в **same** PID namespace (для CUDA driver IPC validation) — `pid: container:<publisher>` (если consumer не имеет PID-1-strict init типа s6-overlay).
4. Иметь NVIDIA runtime — `runtime: nvidia` в compose.
5. Запускаться с правом доступа к socket (по умолчанию root) — `user: root` в compose.
Пример compose service:
```yaml
your-cctv-backend:
image: your-image:cuda
runtime: nvidia
user: root # socket в publisher container root-owned
ipc: "container:cuframes-pub-parking"
pid: "container:cuframes-pub-parking" # если ваш image не использует s6
environment:
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
volumes:
- cuframes_sock:/run/cuframes:ro
```
## См. также
- [filter/README.md](../../filter/README.md) — FFmpeg demuxer (если ваш processor построен на FFmpeg)
- [docs/integrations/frigate.md](frigate.md) — Frigate-specific guide
- [docs/architecture.md](../architecture.md) — внутренности CUDA IPC
- [Полный код CuframesSource](https://git.goldix.org/gx/cctv/src/branch/enterprise/develop/cpp/apps/cctv-processor/src/sources/CuframesSource.cpp) — реальный production-tested файл
+364
View File
@@ -0,0 +1,364 @@
# Frigate integration
Полный production-tested guide для интеграции cuframes с
[Frigate NVR](https://github.com/blakeblackshear/frigate). На основе реального
deployment (Frigate 0.17.1-tensorrt + RTX 5090 + Dahua HEVC камеры).
## Что вы получаете
- **Один NVDEC decode на камеру** вместо одного у Frigate + одного у каждого
другого consumer'а (cctv-processor, AI-скрипт, mosaic-сервер).
- Frigate видит decoded frames через **обычный FFmpeg URL** — никакого fork'а
Frigate-кода. Frigate сам не подозревает что под капотом cuframes.
## Что вы НЕ получаете в v0.1
- **Record path** (`-c:v copy` для архива) — этот path в Frigate всё ещё через
свой отдельный RTSP. v0.2 cuframes решит это через encoded packet sharing
(см. [issue #2](https://git.goldix.org/gx/cuframes/issues/2)).
- Hwaccel CUDA filters для detect resize (`scale_cuda`) — наш minimal FFmpeg
собран без `--enable-cuda-llvm` (не работает на glibc < 2.38 что у Debian 12,
на котором Frigate base). Workaround: `hwaccel_args: []` в config → CPU
scale (cost ~5-10% CPU на FHD25).
## Архитектура
```
Camera RTSP ──► cuframes-rtsp-source ──► [NVDEC ─► NV12 in CUDA IPC]
├──► Frigate (ffmpeg -f cuframes) → detect
├──► cctv-processor (CuframesSource) → motion+mosaic
└──► AI-script (Python ctypes) → inference
```
## Требования
| | Минимум | Note |
|---|---|---|
| NVIDIA driver | 555+ | для CUDA 12 runtime |
| CUDA Toolkit (для build patched FFmpeg) | 12.4+ | host или builder container |
| GPU compute capability | ≥ 7.5 | требование CUDA IPC |
| OS на target (Frigate runtime) | Debian 12 bookworm | glibc 2.36 — это база Frigate `stable-tensorrt` |
| OS на builder | Ubuntu 22.04 (glibc 2.35) | forward-compat с Debian 12 |
| docker buildx | latest | для multi-stage build |
## Шаг 1 — Build patched Frigate image
Cuframes integration требует patched FFmpeg внутри Frigate с `cuframes://`
demuxer. Самый простой путь — собрать overlay image поверх existing Frigate.
### 1.1. Минимальный Dockerfile (Debian 12 builder + custom FFmpeg)
```dockerfile
# Build patched FFmpeg на Debian 12 (glibc-совместимо с Frigate runtime)
FROM debian:bookworm AS builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential cmake git nasm pkg-config ca-certificates wget patch ninja-build \
libssl-dev libx264-dev libx265-dev libnuma-dev zlib1g-dev \
libfreetype-dev libfribidi-dev libharfbuzz-dev libfontconfig-dev \
libvpx-dev libopus-dev libmp3lame-dev libvorbis-dev libtheora-dev libwebp-dev \
libaom-dev libdav1d-dev libsvtav1enc-dev \
libssh-dev librist-dev libsrt-openssl-dev \
libdrm-dev libva-dev libxcb1-dev \
&& rm -rf /var/lib/apt/lists/*
# CUDA toolkit 12.x
RUN wget -q https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb \
&& dpkg -i cuda-keyring_1.1-1_all.deb && rm cuda-keyring_1.1-1_all.deb \
&& apt-get update && apt-get install -y --no-install-recommends cuda-toolkit-12-6 \
&& rm -rf /var/lib/apt/lists/*
ENV PATH=/usr/local/cuda/bin:$PATH
# nv-codec-headers (для FFmpeg ffnvcodec/nvenc/nvdec)
RUN git clone --depth 1 --branch n12.2.72.0 https://github.com/FFmpeg/nv-codec-headers.git /tmp/nvc \
&& make -C /tmp/nvc install && rm -rf /tmp/nvc
# Build libcuframes (static install в /opt/cuframes)
RUN git clone --depth 1 https://git.goldix.org/gx/cuframes.git /src/cuframes \
&& cmake -B /src/cuframes/build -S /src/cuframes -G Ninja \
-DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF \
-DBUILD_EXAMPLES=OFF -DBUILD_TOOLS=OFF \
&& cmake --build /src/cuframes/build -j"$(nproc)" \
&& cmake --install /src/cuframes/build --prefix /opt/cuframes
# Clone patched FFmpeg fork (либо upstream + apply patch — см. filter/README.md)
RUN git clone --depth 1 --branch n7.1-cuframes \
https://git.goldix.org/gx/ffmpeg-patched.git /src/ffmpeg
# Configure (minimal-but-functional для Frigate)
RUN cd /src/ffmpeg && ./configure \
--prefix=/opt/ffmpeg \
--enable-gpl --enable-version3 --enable-nonfree \
--enable-libcuframes \
--enable-libx264 --enable-libx265 \
--enable-libvpx --enable-libopus --enable-libmp3lame \
--enable-libvorbis --enable-libtheora --enable-libwebp \
--enable-libaom --enable-libdav1d --enable-libsvtav1 \
--enable-libfreetype --enable-libfribidi --enable-libharfbuzz \
--enable-libssh --enable-librist --enable-libsrt \
--enable-openssl \
--enable-ffnvcodec --enable-cuvid --enable-nvenc --enable-nvdec \
--extra-cflags="-I/opt/cuframes/include -I/usr/local/cuda/include" \
--extra-ldflags="-L/opt/cuframes/lib -L/usr/local/cuda/lib64" \
--extra-libs="-lcudart -lpthread -lrt -lm" \
--disable-doc --disable-htmlpages --disable-manpages
RUN cd /src/ffmpeg && make -j"$(nproc)" && make install
# ─── Runtime: Frigate + наши binaries поверх ──────────────────────────
FROM ghcr.io/blakeblackshear/frigate:stable-tensorrt
# Missing dynamic .so которые требует наш patched ffmpeg (Frigate image их не имеет —
# bundled статически собран без них в DT_NEEDED)
RUN apt-get update && apt-get install -y --no-install-recommends \
libharfbuzz0b libfribidi0 librist4 libsrt1.5-openssl libssh-4 \
libvpx7 libwebpmux3 libwebp7 libdav1d6 libaom3 libmp3lame0 \
libsvtav1enc1 libtheora0 libvorbis0a libvorbisenc2 \
libx264-164 libx265-199 libopus0 \
&& rm -rf /var/lib/apt/lists/*
# Replace bundled ffmpeg (оригинал backup'нем под .orig)
RUN cp /usr/lib/ffmpeg/7.0/bin/ffmpeg /usr/lib/ffmpeg/7.0/bin/ffmpeg.orig \
&& cp /usr/lib/ffmpeg/7.0/bin/ffprobe /usr/lib/ffmpeg/7.0/bin/ffprobe.orig
COPY --from=builder /opt/ffmpeg/bin/ffmpeg /usr/lib/ffmpeg/7.0/bin/ffmpeg
COPY --from=builder /opt/ffmpeg/bin/ffprobe /usr/lib/ffmpeg/7.0/bin/ffprobe
COPY --from=builder /opt/cuframes/lib/libcuframes.so.0.1.0 /usr/local/lib/
RUN cd /usr/local/lib && ln -sf libcuframes.so.0.1.0 libcuframes.so.0 \
&& ln -sf libcuframes.so.0 libcuframes.so && ldconfig
# Build-time smoke: ldd resolved + cuframes demuxer registered
RUN ldd /usr/lib/ffmpeg/7.0/bin/ffmpeg | grep -q "not found" && exit 1 || true
RUN /usr/lib/ffmpeg/7.0/bin/ffmpeg -hide_banner -formats | grep -q cuframes \
&& echo "OK: cuframes demuxer registered in Frigate image"
```
Build:
```bash
docker build -t local/frigate-cuframes:latest -f Dockerfile.frigate .
```
Размер ~10 GB (наследует Frigate `stable-tensorrt` ~9 GB).
## Шаг 2 — docker-compose: publisher + Frigate
```yaml
services:
# Один publisher на камеру — единственный source RTSP, делает 1× NVDEC.
cuframes-pub-parking:
image: git.goldix.org/gx/cuframes:0.1 # либо local build из filter/Dockerfile.runtime
container_name: cuframes-pub-parking
restart: unless-stopped
runtime: nvidia
# CRITICAL: ipc=shareable — Frigate и другие consumers подсоединяются через
# ipc: container:cuframes-pub-parking
ipc: shareable
shm_size: 256m
environment:
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
volumes:
- cuframes_sock:/run/cuframes
command:
- /usr/local/bin/cuframes-rtsp-source
- --rtsp
- "rtsp://admin:${CAM_PASS}@cam-parking-ip:554/cam/realmonitor?channel=1&subtype=1"
- --key
- cam-parking
- --ring
- "6"
- --verbose
frigate:
image: local/frigate-cuframes:latest
container_name: frigate
restart: unless-stopped
depends_on:
cuframes-pub-parking:
condition: service_started
runtime: nvidia
privileged: true
shm_size: 512m
# CUDA IPC c publisher'ом: shared /dev/shm
# WARN: pid намерено НЕ share'ится — Frigate использует s6-overlay,
# которое требует PID 1 в своём namespace.
ipc: "container:cuframes-pub-parking"
environment:
FRIGATE_RTSP_PASSWORD: "${FRIGATE_RTSP_PASSWORD}"
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
ports:
- "5000:5000"
- "8971:8971"
volumes:
- cuframes_sock:/run/cuframes:ro
- ./config/config.yml:/config/config.yml:ro
- /home/user/frigate-media:/media/frigate
# ... остальные volumes как обычно
volumes:
cuframes_sock:
```
## Шаг 3 — Frigate config.yml
Ключевые отличия от стандартного config:
```yaml
ffmpeg:
# ВАЖНО: hwaccel cuda отключаем (наш ffmpeg без cuda-llvm → нет scale_cuda).
# Detect-path использует CPU scale, но decode уже done у publisher'а.
hwaccel_args: []
output_args:
record: preset-record-generic-audio-aac
cameras:
parking_overview:
enabled: true
ffmpeg:
inputs:
# main (full-res) — только запись в архив через прямой RTSP
# (decode у Frigate НЕ происходит — это `-c:v copy` мux)
- path: rtsp://admin:${FRIGATE_RTSP_PASSWORD}@cam-parking-ip:554/cam/realmonitor?channel=1&subtype=0
roles: [record]
# sub-stream → через cuframes (decoded у publisher'а, без второго NVDEC у Frigate)
- path: cuframes://cam-parking
input_args: -f cuframes
roles: [detect]
detect:
width: 640
height: 480
fps: 5
```
После v0.2 cuframes (encoded packet sharing) record-path тоже мoжет
переключиться на `cuframes_packets://cam-parking` — тогда **никакого RTSP в
Frigate config'е вообще**.
## Шаг 4 — Run + verify
```bash
docker compose up -d
docker logs -f frigate
```
Что искать в logs:
- `[INFO] Camera processor started for parking_overview` — normal startup
- НЕТ `[ERROR] Ffmpeg process crashed` — если есть, посмотри
[Troubleshooting](#troubleshooting)
- В `nvidia-smi dmon -s u` колонка `%dec` должна показывать ~1-2% на одну
камеру (это publisher), Frigate сам не decode'ит cuframes input
```bash
# Проверить что Frigate реально читает cuframes:
docker exec frigate ps -ef | grep ffmpeg | grep cuframes
# Должна быть линия вида:
# ffmpeg ... -f cuframes -i cuframes://cam-parking -r 5 -vf fps=5,scale=640:480 ...
```
## Troubleshooting
### `s6-overlay-suexec: fatal: can only run as pid 1`
Появляется если попытались добавить `pid: container:cuframes-pub-parking` в
Frigate service. Frigate's s6-overlay strict требует PID 1.
**Fix**: убрать `pid:` из compose. Если только `ipc:` shared — большинство
случаев работают (Frigate подсоединяется первым и его CUDA context служит
для последующих).
**Альтернатива**: запустить Frigate с собственным namespace но дублировать
publisher socket через bind-mount. Frigate сам управляется first CUDA context.
### `[AVFilterGraph] No such filter: 'scale_cuda'`
Frigate config имеет `hwaccel_args: preset-nvidia` (default). Наш patched
ffmpeg собран без `--enable-cuda-llvm` (не работает на glibc < 2.38). Эта
опция компилирует CUDA filters, включая `scale_cuda`.
**Fix**: `hwaccel_args: []` в config.yml. CPU scale (5-10% CPU per FHD25 камера).
**Real fix** (planned): cuframes v0.2 — publisher сам делает resize до detect-size
и публикует pre-scaled frames. Тогда Frigate не нуждается в scale_cuda.
### `cudaIpcOpenEventHandle: invalid device context`
Consumer container не имеет shared pid namespace с publisher'ом → CUDA driver
не валидирует IPC peer.
**Fix для cross-container CUDA IPC**: `pid: container:<publisher>` + `ipc:
container:<publisher>`. Для Frigate этот fix недоступен (см. предыдущий пункт).
Workaround — поднять Frigate первым после publisher (race window) или использовать
encoded packet path (v0.2).
### `Nonmatching transport in server reply` от RTSP-output Frigate
Не относится к cuframes — это нормальное поведение Frigate's go2rtc для
TCP transport. TV/VLC обычно использует UDP — оно работает.
## v0.2: dual-input (detect + record через один RTSP)
После cuframes v0.2 publisher активирует **encoded packet ring** параллельно
с decoded frames ring. Это даёт Frigate одновременно:
- `cuframes://<key>`**decoded NV12** для `detect` role (как в v0.1)
- `cuframes_packets://<key>`**encoded H.264/H.265** для `record` role
(passthrough, без decode)
**1 RTSP connection** к камере вместо 2-3 (Frigate сейчас открывает
отдельный stream для record).
### Setup
```bash
cuframes-rtsp-source \
--rtsp rtsp://admin:pw@192.168.88.98/cam/realmonitor?channel=1 \
--key cam-parking \
--enable-packet-ring
```
Publisher держит **два** SHM:
- `/dev/shm/cuframes-cam-parking` (decoded NV12, v0.1)
- `/dev/shm/cuframes-cam-parking-packets` (encoded packets, v0.2)
### Frigate config
```yaml
cameras:
cam_parking:
ffmpeg:
inputs:
- path: cuframes://cam-parking
input_args: -f cuframes
roles: [detect]
- path: cuframes_packets://cam-parking
input_args: -f cuframes_packets
roles: [record]
```
### Requirements
- Patched FFmpeg с обоими demuxer'ами:
[gx/ffmpeg-patched PR #1](https://git.goldix.org/gx/ffmpeg-patched/pulls/1).
- Frigate Dockerfile перекомпилирован с этим ffmpeg (см. секцию выше про
`cuframes-frigate:0.17` build).
### Trade-offs
| Метрика | v0.1 (frames only) | v0.2 (frames + packets) |
|---|---|---|
| RTSP к камере | 1 (publisher) | 1 (publisher) |
| Frigate-side RTSP | 1+ (record отдельно) | **0** — всё через cuframes |
| Camera RTSP streams | 2+ | **1** |
| Доп. VRAM | ring (~10 MB) | без изменений |
| Доп. host RAM | минимум | + 8 MB на packet ring |
| Доп. CPU | nominal | nominal (memcpy в shared ring) |
## См. также
- [filter/README.md](../../filter/README.md) — детали FFmpeg demuxer + patch
- [docs/integration.md](../integration.md) — общий integration guide
- [docs/protocol.md §10](../protocol.md#10-v02-extension-encoded-packet-ring-proto_version2) — wire-protocol spec для packet ring
- [BENCHMARKS.md](../../BENCHMARKS.md) — production-measured результаты
- [ROADMAP.md](../../ROADMAP.md) — v0.3+ planned features
+47
View File
@@ -0,0 +1,47 @@
# Launch drafts
Drafts для outreach / launch. Все — **draft material**, перед отправкой review.
## Порядок (рекомендуемый)
1. **`frigate-integration-issue.md`** — soft-launch, низкий риск отказа, целевая
аудитория уже жалуется на проблему в 3 discussion'ах. Может дать первых
early-adopter'ов и social proof для следующего шага.
2. **`ffmpeg-devel-rfc.md`** — после того как Frigate-discussion получит
позитивный engagement (даже один "+1, would use" комментарий — уже traction).
Mailing-list FFmpeg-devel предъявляет высокий стандарт; готовиться тщательно.
3. **`hn-show-post.md`** — финальный, после того как либо RFC получит первый
response, либо ясно что молчат. HN — это amplifier, не starting line.
## Что в каждом draft
| Файл | Куда | Формат | Когда |
|---|---|---|---|
| [`frigate-integration-issue.md`](frigate-integration-issue.md) | github.com/blakeblackshear/frigate | Discussion (Ideas category) | Сейчас |
| [`ffmpeg-devel-rfc.md`](ffmpeg-devel-rfc.md) | `ffmpeg-devel@ffmpeg.org` | Patch + cover letter via `git send-email` | После Frigate engagement |
| [`hn-show-post.md`](hn-show-post.md) | news.ycombinator.com | Show HN | Etap F (finale) |
## Что **не** делать
- Не публиковать всё сразу в один день — невозможно отвечать на all-channels параллельно.
- Не публиковать в выходные / праздники / во время большого tech-event (Apple keynote, GTC, etc).
- Не упоминать "AI", "battle-tested", "production-ready", "enterprise" в тексте — все эти аудитории (FFmpeg-devel, Frigate, HN) аллергичны к маркетинговому языку.
- Не публиковать FFmpeg patch **без** sign-off — automatic rejection.
- Не отправлять HN-пост если не можешь быть онлайн первые 2 часа после публикации — ранжирование умрёт.
## Что подготовить перед отправкой
- [ ] Subscribe на ffmpeg-devel (https://ffmpeg.org/mailman/listinfo/ffmpeg-devel) — иначе reply'ы не получишь
- [ ] `git config --global` для send-email (см. ffmpeg-devel-rfc.md шаги)
- [ ] Sign-off в FFmpeg commit (`git commit --amend -s` если ещё нет)
- [ ] GitHub аккаунт для Frigate discussion (если нет уже)
- [ ] HN аккаунт с пара дней истории — fresh accounts автоматически шадо-банятся
## После отправки
Следить за reply'ями в течение первой недели. Все три канала — асинхронные, но первые **48 часов** обычно решающие.
Куда смотреть статус engagement:
- ffmpeg-devel: https://ffmpeg.org/pipermail/ffmpeg-devel/
- Frigate discussion: появится в правой панели repo
- HN: https://news.ycombinator.com/threads?id=YOURUSER
+160
View File
@@ -0,0 +1,160 @@
# FFmpeg-devel RFC submission
**Status:** DRAFT — review перед отправкой.
**Куда:** `ffmpeg-devel@ffmpeg.org` (subscribe: https://ffmpeg.org/mailman/listinfo/ffmpeg-devel)
**Как:** patch генерится через `git format-patch`, отправляется `git send-email` с cover-letter. FFmpeg **не использует** GitHub PR / pull-request — только mailing-list patches.
---
## Шаги отправки
```bash
# 1. Конфигурация git send-email (один раз)
git config --global sendemail.smtpserver smtp.gmail.com
git config --global sendemail.smtpserverport 587
git config --global sendemail.smtpencryption tls
git config --global sendemail.smtpuser ВАШ-EMAIL
# password — через ~/.netrc или интерактивно
# 2. На fork ffmpeg-patched, в ветке n7.1-cuframes:
cd /path/to/ffmpeg-patched
git log --oneline n7.1..n7.1-cuframes # должна быть одна commit
# 3. Подготовить .patch
git format-patch -1 --cover-letter --subject-prefix='RFC PATCH' \
--output-directory=/tmp/cuframes-rfc \
n7.1..n7.1-cuframes
# 4. Отредактировать /tmp/cuframes-rfc/0000-cover-letter.patch:
# - Заменить *** SUBJECT HERE *** → см. ниже
# - Заменить *** BLURB HERE *** → cover-letter body (см. ниже)
# 5. Dry-run
git send-email --dry-run --to=ffmpeg-devel@ffmpeg.org /tmp/cuframes-rfc/*.patch
# 6. Реальная отправка
git send-email --to=ffmpeg-devel@ffmpeg.org /tmp/cuframes-rfc/*.patch
```
## Subject line
```
[RFC PATCH 0/1] libavformat/cuframesdec: zero-copy CUDA frame ingest via IPC
```
## Cover-letter body
```
Hi all,
This RFC adds a new demuxer "cuframes" to libavformat that ingests already-
decoded video frames residing in CUDA device memory, produced by another
process via the libcuframes IPC layer [1].
# Why
In multi-consumer GPU video pipelines (CCTV with multiple analytics
services, multi-stream transcoding farms, ML inference + recording on the
same source) every consumer typically runs its own NVDEC session. On 16
cameras × 25 fps × N consumers this multiplies NVDEC sessions, OS
context-switches and host<->device PCIe traffic for what is logically the
same decoded frame.
cuframes addresses this by letting one process decode (e.g. via FFmpeg's
existing CUDA hwaccel) and publish the decoded frames into a small CUDA
ring buffer; other processes import the buffer via cudaIpcOpenMemHandle
and consume the same VRAM allocation without redecoding or copying.
The libavformat demuxer in this RFC is the consumer side: it exposes the
remote ring buffer as a regular AVFormat input source, so any downstream
FFmpeg filter chain or muxer can use it transparently.
# Scope of this patch
libavformat/cuframesdec.c — new demuxer
libavformat/allformats.c — registration
configure — --enable-libcuframes option
The demuxer currently outputs NV12 frames via cudaMemcpy2DAsync to host
memory (rawvideo path). A v0.2 follow-up is planned that emits frames
directly as CUDA AVHWFramesContext (true zero-copy into a CUDA-aware
filter chain) — see [2].
# Out-of-tree library
libcuframes (the producer side, the IPC handshake, the ring-buffer
allocator) lives out-of-tree at [1], licensed LGPL-2.1+ to match FFmpeg.
The demuxer links against libcuframes via pkg-config.
This mirrors the model used by other libavformat plugins that wrap third-
party libraries (libsmbclient, librist, libsrt, etc.).
# Testing
- Unit smoke tests in the libcuframes repo (1 publisher × 4 subscribers ×
2000 frames @ 120 fps — 0 torn frames, 0 gaps).
- E2E test against a real RTSP IP camera (Dahua HEVC 1920×1080, 25 fps,
100/100 frames, avg_fps=25.03).
- ~24h production deployment serving Frigate (object detection) and a
custom analytics pipeline from a single decoder, single NVDEC session.
# Prior art and what this is not
There is no in-tree mechanism for sharing decoded GPU frames between
unrelated FFmpeg processes. Existing alternatives are:
- CUDA hwdownload + hwupload (defeats the purpose — round-trips via PCIe)
- DeepStream Gst-nvstreammux (NVIDIA, closed, GStreamer-only)
- Vendor-locked NVENC/NVDEC pooling helpers
cuframes is intentionally minimal: ring buffer + handshake + IPC handles.
No transcoding logic, no policy.
# Limitations / known issues for review
- NVIDIA GPUs only (CUDA IPC is vendor-specific).
- Linux only (POSIX SHM + AF_UNIX sockets).
- Producer and consumer must share the same CUDA device (CUDA IPC limit).
- NV12 only in v0.1; other pixel formats are roadmap items.
- Driver ≥ 525, CUDA toolkit ≥ 12.0 (≥ 13.0 recommended).
# Feedback wanted
1. Is the libavformat demuxer the right home for this, or would a
hwcontext_cuda extension + a thin demuxer be a better split?
2. Are folks open to an out-of-tree library dependency under
--enable-libcuframes, given the precedent of librist/libsrt?
3. Naming: "cuframes" vs "cudaipcframes" vs something else?
Happy to iterate. Patch follows.
[1] https://git.goldix.org/gx/cuframes (LGPL-2.1+)
[2] https://git.goldix.org/gx/cuframes/issues/2 (v0.2 zero-copy plan)
Signed-off-by: <YOUR NAME> <YOUR EMAIL>
```
## Notes на review
- **Subject prefix `[RFC PATCH]`** — потому что это design discussion, не "merge this now". Если получите конструктивный feedback и сделаете revision — следующая будет `[PATCH v2]`.
- **Sign-off обязателен** — иначе patch отклонят на уровне tooling.
- **Не упоминать** "production-ready", "battle-tested", "30 days of uptime" — FFmpeg-devel список **очень** аллергичен на маркетинговый тон. Numbers OK, эпитеты нет.
- **Не CC** maintainers без приглашения — ответят те, кому интересно. Можно CC Timo Rothenpieler (CUDA hwaccel maintainer) если хочется ускорить — но **только** после первого revision если тишина.
- Возможные возражения:
- "Why not Vulkan video?" — Vulkan video не имеет cross-process sharing API на уровне CUDA IPC. Vulkan external memory работает с DMA-BUF на Linux но требует DRM device sharing, что тоже non-trivial — отдельный RFC материал.
- "Why a new demuxer, not a filter?" — потому что producer уже **вне** этого FFmpeg-процесса; demuxer — это место где AVFormat читает из внешнего источника. Filter pull'ает из upstream AVStream — здесь нет upstream.
## Альтернативный путь — ffmpeg-user (lighter)
Если кажется что для `-devel` сразу с patch'ем тяжело — можно начать с **awareness email** в `ffmpeg-user@ffmpeg.org`:
```
Subject: ANNOUNCE: libcuframes — zero-copy CUDA frame sharing for FFmpeg pipelines
[3 параграфа: what / why / link to repo]
Patch для libavformat будет отправлен в -devel список после feedback от пользователей.
```
Это **soft launch** — мень рисков отказа, больше шансов получить early adopters которые потом support'ят RFC. Рекомендую этот шаг **сначала**.
+115
View File
@@ -0,0 +1,115 @@
# Frigate integration issue
**Status:** DRAFT — review перед публикацией.
**Куда:** https://github.com/blakeblackshear/frigate
**Тип:** GitHub **Discussion** (category: Ideas), **не** Issue. Причина: это feature proposal, не баг. Frigate активно использует discussions (см. [#17033](https://github.com/blakeblackshear/frigate/discussions/17033), [#20191](https://github.com/blakeblackshear/frigate/discussions/20191), [#21559](https://github.com/blakeblackshear/frigate/discussions/21559) — все три уже жалуются на эту проблему).
**Альтернатива:** ответить в одной из существующих discussion'ов о NVDEC saturation. Может быть лучше — там уже собралась audience.
---
## Title
```
[Ideas] Reduce NVDEC duplication on multi-consumer cameras via shared CUDA frame buffer (cuframes)
```
## Body
```markdown
## Problem
When Frigate co-exists with other GPU-using video consumers on the same
camera stream (separate AI processor, custom analytics, recording to a
second NVR, etc.), each process opens its own NVDEC session and decodes
the same H.264/HEVC stream independently. On 16+ cameras at 25 fps this
becomes the bottleneck on consumer GPUs:
- NVDEC sessions are limited (4 concurrent on RTX 30xx/40xx, more on
workstation cards). Decoder context creation / destruction is not free.
- Each duplicate decode burns PCIe bandwidth pushing the same NV12 frame
to host memory (in setups that go through `hwdownload`).
- Power draw and thermals scale with redundant decoding.
Related discussions: #17033, #20191, #21559.
## Existing workarounds
- Single Frigate restream and have everything else pull from go2rtc — works
for re-encoding to TCP/UDP, but every downstream still re-decodes.
- DeepStream `nvstreammux` — solves it but is closed-source NVIDIA stack,
GStreamer-only, not co-installable with current Frigate ffmpeg pipeline.
## Proposal: cuframes ingest source
[cuframes](https://git.goldix.org/gx/cuframes) (LGPL-2.1+) is a small
library that lets one process decode once into a CUDA ring buffer and any
number of other processes import that buffer via CUDA IPC and consume
**zero-copy** in VRAM.
Concretely for Frigate this would mean a new ffmpeg input source like:
```yaml
cameras:
driveway:
ffmpeg:
inputs:
- path: cuframes://driveway
input_args: preset-cuframes
roles: [detect]
```
where a sentinel container (one per camera, ~5MB RAM, runs
`cuframes-rtsp-source`) does the actual RTSP pull + NVDEC and Frigate
attaches to that pre-decoded stream.
## Working integration (early proof)
I've been running this in production for ~24h: a single
`cuframes-rtsp-source` container per camera serves both Frigate
(detection role) **and** a separate C++ analytics pipeline from the same
NVDEC session. Frigate gets pre-decoded NV12 frames; no detection or
recording behaviour was changed.
Integration guide with full docker-compose and a patched Frigate Dockerfile:
https://git.goldix.org/gx/cuframes/src/branch/main/docs/integrations/frigate.md
## What I'm asking for
Not a PR yet — first I'd like maintainer / community input on:
1. Would Frigate be open to **upstream** a `cuframes://` input source, or
should this stay a third-party patched Frigate image?
2. If upstream — what's the preferred shape: new ffmpeg preset only
(zero core code changes), or a first-class `decoder: cuframes` option
in the Frigate config schema?
3. The cuframes library currently requires `--ipc` and `--pid` namespace
sharing between producer and consumer containers. Frigate uses
`s6-overlay` which is incompatible with `--pid` share (s6 needs PID 1).
The current integration uses a small race-window workaround
([troubleshooting #2](https://git.goldix.org/gx/cuframes/src/branch/main/docs/troubleshooting.md));
a cleaner solution requires either making s6 optional in the Frigate
image or moving the IPC handshake to a sidecar pattern.
## Limitations of cuframes (full disclosure)
- NVIDIA GPUs only.
- Linux only.
- Producer + consumer must share the same CUDA device.
- NV12 frame format only in v0.1.
- Requires patching FFmpeg with a small (~400 LOC) demuxer; an upstream
FFmpeg RFC is in flight separately.
If this looks worth pursuing I'm happy to open a draft PR against a feature
branch and iterate.
```
## Notes на review
- **Tone:** Frigate maintainer (Blake) ценит конкретику и production proof — без них любой feature request кладётся в backlog. У нас есть production proof (24h+) — это сильный аргумент, использован прямо.
- **Не обещаем upstream без request'а** — спрашиваем discussion'ом, не PR'ом. Если Blake скажет "не наш scope, оставайтесь third-party" — это OK; integration guide уже валиден как standalone.
- **Прозрачно про s6-overlay constraint** — это блокирующий issue для clean upstream'а. Лучше упомянуть сразу чем спрятать и получить отказ через 2 недели review.
- **Линки на 3 existing discussions** — показывает что problem подтверждена сообществом, не наша одинокая боль.
- **Не упоминать другие AI-системы** (ANPR, face recognition итд) — Blake уже несколько раз говорил что Frigate scope = детектор и NVR, не platform. Подача "cuframes решает вашу проблему" работает лучше чем "cuframes построит экосистему".
+107
View File
@@ -0,0 +1,107 @@
# Show HN post (для Etap F — позже)
**Status:** DRAFT — не публикуем сейчас. Этот файл черновик к Etap F (launch).
**Куда:** https://news.ycombinator.com/submit
**Когда публиковать:**
- После того как FFmpeg-devel RFC получит первый response (даже отказ — это traction)
- ИЛИ после того как Frigate discussion получит +5 upvotes / 3+ комментариев
- ИЛИ если оба молчат 2 недели — публиковать в любом случае, HN-аудитория более независимая
- **Время:** будний день, 13:00-15:00 UTC (peak HN traffic from US morning + EU afternoon)
- **Не публиковать** в пятницу вечером / в выходные / в крупный tech-event день (Apple keynote, GTC, etc.) — drown'ит в шуме
---
## Title
Опции (выбрать одну):
1. `Show HN: Cuframes zero-copy sharing of decoded video frames between processes via CUDA IPC`
2. `Show HN: Stop redecoding the same RTSP stream in every consumer`
3. `Show HN: Cuframes one NVDEC, many consumers, zero-copy in VRAM`
Рекомендую **#2** — describes problem in 7 words, HN любит problem-first titles. #1 — для технической HN ниши тоже OK.
## Body
```markdown
Hi HN,
I run a homelab CCTV stack with 16 cameras feeding into Frigate (object
detection), a custom C++ analytics service, and a recording NVR. All three
were running NVDEC on the same RTSP streams. On an RTX 3060 this saturated
the decoder slots and the consumer GPUs in my office burnt about 40W of
redundant decoding when nothing interesting was happening.
So I wrote a small library that lets one process decode the stream once
into a CUDA ring buffer and the others import the same buffer via
cudaIpcOpenMemHandle. Decoded NV12 frame lands in VRAM exactly once, every
consumer reads it zero-copy.
Repo (LGPL-2.1+): https://git.goldix.org/gx/cuframes
What's in it:
- libcuframes — the producer/consumer C/C++ library
- cuframes-rtsp-source — standalone RTSP → cuframes bridge (one per cam)
- A small out-of-tree FFmpeg demuxer ("cuframes://") so downstream
consumers don't need to know they're consuming shared frames
- Reference docker-compose for the Frigate + custom-app setup
- 24h production deployment on the homelab, ~25 fps × 16 cameras × 3
consumers from a single NVDEC session
What surprised me along the way:
- CUDA IPC handles are bound to the device that allocated them, not just
a CUDA context — both peers must be on the same GPU. (Documented;
bit out of the way in the Programming Guide §3.2.8.)
- Cross-container CUDA IPC needs both --ipc and --pid namespace share,
not just --ipc. The latter wasn't obvious from the error message
("invalid device context" with no mention of /proc visibility).
- Frigate's s6-overlay is incompatible with --pid share because s6
insists on being PID 1. There's a documented race-window workaround
but it's the one rough edge.
What it is not:
- Not a transcoding framework. No re-encoding, no filtering, no policy.
- Not multi-GPU (CUDA IPC is single-device).
- Not Windows / macOS / WSL2 / AMD.
What's next:
- Upstream FFmpeg RFC for the demuxer (drafted, not sent yet — would
appreciate review of the RFC text first).
- v0.2 makes the FFmpeg path true zero-copy via AVHWFramesContext (no
cudaMemcpy2DAsync round-trip).
Happy to answer questions. Especially interested in:
- Anyone running multi-consumer GPU video pipelines with a different
solution? Curious what tradeoffs you hit.
- Vulkan-video folks: is there an obvious cross-process sharing path
via VkExternalMemory + DMA-BUF that I'm missing? I went CUDA-only
because that's what worked first, but Vulkan would be vendor-neutral.
— [your handle]
```
## Notes на review
- **HN формат:** первая строка — hook (concrete problem, concrete numbers — "40W redundant decoding"). НЕ начинать с "Hi everyone, today I'm excited to share..."
- **Без emoji**, без markdown headers (HN не renders'ит markdown в title-area; body тоже почти plain text)
- **Конкретные числа** — HN respect'ит numbers. "40W", "24h", "25 fps × 16 cam × 3 consumer", "~400 LOC patch"
- **"What it is not"** — отсекает Vue Apologists которые иначе пишут "why don't you support Windows?". Это HN best practice
- **Open questions внизу** — driver discussion. Без них первый комментарий = "и зачем это?". С ними — "вот мой опыт с DeepStream"
- **Avoid:** "battle-tested", "production-ready", "enterprise-grade", "10x faster than X" — HN crowd специально downvotes такое
- **Будь готов** отвечать **первые 2 часа** активно — HN ранжирование сильно зависит от engagement в первый час. Если не сможешь быть в офлайне — не публикуй
- **Если автор — не main maintainer** repo — упомянуть это в первом комменте от собственного аккаунта чтобы не выглядело как третье-лицо PR
## Альтернатива — r/selfhosted
Если HN кажется слишком high-stakes, можно сначала **r/selfhosted** (180k subs) — там Frigate-аудитория, прямой fit. Менее brutal, легче получить early feedback.
Title для reddit: `Reduced NVDEC saturation across Frigate + custom apps by sharing decoded frames over CUDA IPC — open-sourced the library`
Этот текст короче (HN body слишком длинный для reddit), но идея та же.
+397
View File
@@ -423,3 +423,400 @@ TEST(Handshake, HelloRespMismatchProto) {
`libcuframes/src/protocol.c` (Phase 1, Step 2) — единственная reference.
Любая другая реализация (Python ctypes, Rust bindings, FFmpeg plugin)
должна **conformance-tested** против этого документа.
## 10. v0.2 extension: encoded packet ring (proto_version=2)
**Статус:** design draft, ещё не реализовано (см. issue #2).
Параллельно с decoded-frames ring (§2) publisher может опционально
поддерживать **encoded packet ring** — публикует raw H.264/H.265 NAL units
**до** decoder, для consumer'ов которые делают `-c:v copy` (recording, mux).
### 10.1 Совместимость с v1
- v2 publisher принимает **v1-subscribers** — они получают только frames
ring (как v0.1), packet ring им не показывается.
- v1 publisher отвергает v2-subscribers с `wants_packets=true`
(HELLO_RESP error PROTOCOL).
- v1 layout (§2) **не меняется** для frames ring — packet ring это отдельный SHM.
Publisher version bumping:
- `proto_version` = 2 в SHM header и в HELLO_RESP когда packet ring active.
- Если publisher v2 не активирует packet ring (`enable_packet_ring=false`)
`proto_version` остаётся 1 (полная v1 compat).
### 10.2 Дополнительные ресурсы
| Resource | Path | Назначение | Когда |
|---|---|---|---|
| Packet shared memory | `/dev/shm/cuframes-<key>-packets` | Packet ring header + slots + byte buffer | если publisher активировал packet ring |
Cleanup — симметрично §1: `shm_unlink` при destroy(); orphaned автоматически
если nobody mmap'ит.
### 10.3 Packet ring layout
Размер пакетного SHM: `sizeof(packet_ring_header_t) + N×PSE + DATA_SIZE`,
где:
- N = `packet_ring_slots`, default 64 (configurable)
- PSE = `sizeof(packet_slot_entry_t)` = 64 байт (см. §10.5)
- DATA_SIZE = `packet_data_size`, default 8 MB (configurable)
#### Byte layout
```
Offset Size Field Comments
─────────────────────── ────── ────────────────────────── ─────────────────────────────
0x0000 4 magic (LE u32) 0xCC7C1DCD (frames magic + 1)
0x0004 4 proto_version (LE u32) 2
0x0008 4 ring_slots (LE u32) N (1..1024)
0x000C 4 data_size (LE u32) bytes for packet data ring
0x0010 4 codec_id (LE u32) AV_CODEC_ID_* enum (см. §10.4)
0x0014 4 codec_extradata_size (LE u32) ≤ 4096
0x0018 8 producer_pid (LE u64)
0x0020 8 global_seq (LE u64, atomic) монотонная по packets
0x0028 8 last_keyframe_seq (LE u64, atomic) для late subscribers
0x0030 8 write_offset (LE u64, atomic) текущий cursor в data ring
0x0038 8 shutdown_flag (LE u64, atomic)
0x0040 4096 codec_extradata SPS/PPS/VPS bytes (см. §10.4)
0x1040 N×64 slots[N] packet_slot_entry_t (см. §10.5)
0x1040+N×64 DATA_SIZE data[] wraparound byte buffer
```
Все atomic fields — C11 `_Atomic` (release/acquire semantics для seq updates).
### 10.4 Codec extradata
H.264 — SPS + PPS, конкатенированные в **Annex B** формате
(start codes `00 00 00 01`). H.265 — VPS + SPS + PPS.
`codec_id` соответствует FFmpeg `AV_CODEC_ID_H264`, `AV_CODEC_ID_HEVC`,
`AV_CODEC_ID_AV1` (future). Subscriber пишет этот extradata в
`AVCodecContext.extradata` своего decoder'а (если он его создаёт)
или в `AVStream.codecpar->extradata` для muxer'ов.
Extradata устанавливается publisher'ом **один раз** при первом keyframe
(или из RTSP SDP до первого packet). После — fixed на lifetime publisher'а
(codec change mid-stream → publisher destroy+recreate с новым `<key>`).
### 10.5 Packet slot entry (64 байта)
```
Offset Size Field Comments
0x00 8 seq (LE u64, atomic) published seq; UINT64_MAX = invalid
0x08 8 pts_ns (LE i64)
0x10 8 dts_ns (LE i64) для B-frames pipelines
0x18 8 data_offset (LE u64) offset в `data[]` секции SHM
0x20 4 data_size (LE u32) size of payload bytes
0x24 4 flags (LE u32) §10.6
0x28 24 reserved 0
```
`data_offset` может быть **больше** `data_size` секции SHM — semantics
"absolute byte cursor", фактический byte index = `data_offset % data_size`.
Subscriber может detect wrap (если payload crosses end → split read).
### 10.6 Packet flags
```
Bit Name Comments
0 KEY keyframe (IDR for H.264, или CRA/IDR для HEVC).
Critical для late subscribers — must wait IDR.
1 CORRUPT publisher detect'нул что packet damaged
(RTP loss и т.п.) — subscriber может skip
2 DISCONTINUITY был gap перед этим packet
(publisher reconnect к камере)
3 LAST_IN_AU last NAL в access unit (полный frame)
— для muxer'ов которые ждут полный frame
4-31 reserved 0
```
Mapping в `AVPacket.flags`:
- bit 0 (KEY) → `AV_PKT_FLAG_KEY`
- bit 1 (CORRUPT) → `AV_PKT_FLAG_CORRUPT`
- bit 2 (DISCONTINUITY) → `AV_PKT_FLAG_DISCONTINUITY` (FFmpeg 5+)
### 10.7 Atomic publish (publisher-side)
```c
// Pseudo-C (упрощено, без error handling)
uint64_t seq = atomic_load(&hdr->global_seq, RELAXED) + 1;
uint64_t off = atomic_load(&hdr->write_offset, RELAXED);
// 1. Найти free slot (overwrite oldest)
size_t slot_idx = seq % hdr->ring_slots;
packet_slot_entry_t *slot = &slots[slot_idx];
// 2. Записать payload bytes (wraparound, может потребовать 2 memcpy)
size_t off_in_ring = off % hdr->data_size;
size_t first_chunk = min(size, hdr->data_size - off_in_ring);
memcpy(data + off_in_ring, payload, first_chunk);
if (first_chunk < size)
memcpy(data, payload + first_chunk, size - first_chunk);
// 3. RELEASE: записать metadata в slot
slot->pts_ns = pts;
slot->dts_ns = dts;
slot->data_offset = off;
slot->data_size = size;
slot->flags = flags;
atomic_store(&slot->seq, seq, RELEASE);
// 4. Update global cursor + global_seq
atomic_store(&hdr->write_offset, off + size, RELEASE);
atomic_store(&hdr->global_seq, seq, RELEASE);
// 5. If KEY → update last_keyframe_seq
if (flags & PKT_FLAG_KEY)
atomic_store(&hdr->last_keyframe_seq, seq, RELEASE);
```
### 10.8 Atomic read (subscriber-side)
```c
// Pseudo-C
uint64_t cur = atomic_load(&hdr->global_seq, ACQUIRE);
if (cur <= my_last_seq) return TIMEOUT; // ничего нового
uint64_t want_seq = my_last_seq + 1;
size_t slot_idx = want_seq % hdr->ring_slots;
packet_slot_entry_t *slot = &slots[slot_idx];
uint64_t slot_seq = atomic_load(&slot->seq, ACQUIRE);
if (slot_seq != want_seq) {
// overrun — slow subscriber. Re-anchor:
want_seq = atomic_load(&hdr->last_keyframe_seq, ACQUIRE);
slot_idx = want_seq % hdr->ring_slots;
slot = &slots[slot_idx];
return DROPPED; // signal user через flags = DISCONTINUITY
}
// Copy payload (wraparound aware)
uint64_t off = slot->data_offset % hdr->data_size;
uint32_t size = slot->data_size;
uint32_t first_chunk = min(size, hdr->data_size - off);
memcpy(out_buf, data + off, first_chunk);
if (first_chunk < size)
memcpy(out_buf + first_chunk, data, size - first_chunk);
// Re-check slot->seq не изменился (защита от overrun mid-read)
if (atomic_load(&slot->seq, ACQUIRE) != want_seq) {
return DROPPED; // publisher overwrote во время copy
}
my_last_seq = want_seq;
return OK;
```
Защита от overrun mid-read через **post-check `slot->seq`** — простая
вариант seqlock. Если publisher успел overwrite между metadata-read и
data-copy — subscriber detect и retry.
### 10.9 Socket protocol extensions
#### HELLO_REQ — добавляются flags в reserved field
v1 layout (§3.3):
```
[4 bytes] proto_version
[4 bytes] consumer_name_len
[N bytes] consumer_name
[4 bytes] cuda_device
[4 bytes] mode
[12 bytes] reserved (must be 0) ← v0.2 использует первые 4 байта
```
v0.2 интерпретирует первые 4 байта `reserved` как `subscribe_flags`:
| Bit | Name | Comments |
|---|---|---|
| 0 | `WANTS_FRAMES` | подписаться на decoded frames ring (default ON в v1 — implicit) |
| 1 | `WANTS_PACKETS` | подписаться на encoded packet ring |
| 2-31 | reserved | 0 |
Если v1-subscriber оставляет reserved=0 — publisher v2 интерпретирует это
как `WANTS_FRAMES=true, WANTS_PACKETS=false` (v1 backward-compat).
#### HELLO_RESP — добавляются packet-ring fields
v1 layout (§3.4) расширяется в reserved секции:
```
[4 bytes] result
[4 bytes] proto_version_actual ← теперь может быть 1 или 2
[4 bytes] ring_size ← frames ring
[4 bytes] ownership_mode
[64 bytes] frame_meta
[4 bytes] shm_path_len ← frames SHM
[N bytes] shm_path
[12 bytes] reserved ← v0.2 интерпретирует
```
v0.2 reserved layout (если `proto_version_actual == 2` И publisher
поддерживает packets):
```
[4 bytes] packet_shm_path_len (LE u32) 0 = packets disabled at publisher
[N bytes] packet_shm_path (UTF-8) — относительно /dev/shm/, например "cuframes-camA-packets"
[4 bytes] codec_id (LE u32) AV_CODEC_ID_*
[4 bytes] initial_packet_seq (LE u64) last_keyframe_seq на момент handshake
(subscriber должен start с этого seq)
```
Если subscriber запросил `WANTS_PACKETS=1` но publisher не имеет packet ring
`result = ERR_NOT_AVAILABLE`.
### 10.10 Subscriber state machine extension
Подключение к **обоим** rings (или одному из):
```
┌──────────┐
│ HELLO_OK │ proto_version_actual=2, packet_shm_path_len>0
└────┬─────┘
┌────────────────────────────────┐
│ Open frames SHM (если WANTS_FRAMES) │ → standard v1 flow
└────────────────────────────────┘
┌────────────────────────────────┐
│ Open packet SHM (если WANTS_PACKETS) │
│ - mmap /dev/shm/cuframes-<key>-packets │
│ - check magic, proto_version │
│ - set my_last_packet_seq = initial_packet_seq - 1 │
│ (так что первый next_packet вернёт IDR) │
└────────────────────────────────┘
┌─────────┐
│ READY │ — frames или packets или оба доступны
└─────────┘
```
### 10.11 Threading в subscriber
Frames ring и packet ring имеют **разные** `global_seq` counters.
Subscriber имеет **отдельные** `my_last_seq` для каждого. Может
poll'ить обе независимо (или через два threads).
Producer's `cudaEventRecord` (frames sync) не релевантен для packets —
encoded data на CPU, без CUDA sync.
### 10.12 Конфигурируемость packet ring
Publisher API extension (§10.13) принимает параметры:
```c
typedef struct {
uint32_t packet_ring_slots; // default 64
uint32_t packet_data_size; // default 8 MB (8388608)
uint32_t max_packet_size; // default 2 MB — sanity guard для оversized
// packets (publisher rejects with error)
uint32_t codec_id; // AV_CODEC_ID_H264 / HEVC / ...
} cuframes_packet_ring_options_t;
```
### 10.13 API extension (для cuframes.h)
```c
/* Сreate publisher с активным packet ring. NULL для opts → packet ring disabled. */
int cuframes_publisher_create_ex(
const cuframes_publisher_options_t *frames_opts,
const cuframes_packet_ring_options_t *packet_opts, /* NULL = no packet ring */
cuframes_publisher_t **pub_out
);
/* Set codec extradata (SPS/PPS) — должен быть called до первого publish_packet. */
int cuframes_publisher_set_codec_extradata(
cuframes_publisher_t *pub,
const void *extradata,
size_t size
);
/* Публикация packet. Slow consumer = overwrite oldest. */
int cuframes_publisher_publish_packet(
cuframes_publisher_t *pub,
const void *data,
size_t size,
int64_t pts_ns,
int64_t dts_ns,
uint32_t flags /* CUFRAMES_PKT_FLAG_KEY | _CORRUPT | _DISCONTINUITY | _LAST_IN_AU */
);
/* Subscriber-side: подписаться с opt-in для packets. */
typedef struct {
/* ... existing v1 fields ... */
uint32_t subscribe_flags; /* WANTS_FRAMES, WANTS_PACKETS bits */
} cuframes_subscriber_options_v2_t;
int cuframes_subscriber_create_v2(
const cuframes_subscriber_options_v2_t *opts,
cuframes_subscriber_t **sub_out
);
/* Чтение packet. Opaque handle — каллер вызывает release_packet после. */
typedef struct cuframes_packet cuframes_packet_t;
int cuframes_subscriber_next_packet(
cuframes_subscriber_t *sub,
cuframes_packet_t **pkt_out,
int32_t timeout_ms
);
const void * cuframes_packet_data(const cuframes_packet_t *p);
size_t cuframes_packet_size(const cuframes_packet_t *p);
int64_t cuframes_packet_pts(const cuframes_packet_t *p);
int64_t cuframes_packet_dts(const cuframes_packet_t *p);
uint32_t cuframes_packet_flags(const cuframes_packet_t *p);
int cuframes_subscriber_release_packet(cuframes_subscriber_t *sub, cuframes_packet_t *p);
/* Codec params для subscriber (extracted из shared header). */
int cuframes_subscriber_get_codec_params(
cuframes_subscriber_t *sub,
uint32_t *codec_id_out,
const void **extradata_out,
size_t *extradata_size_out
);
```
`cuframes_packet_t` opaque — фактически указатель в local-mapped data (на
heap subscriber'а — copy при `next_packet`, освобождение при `release`).
Subscriber **не** держит ссылки на shared ring data между `next_packet` и
`release_packet` — это избавляет от reader-locks.
### 10.14 Late subscriber → keyframe-aligned start
При SUBSCRIBE_RESP publisher отвечает `initial_packet_seq = last_keyframe_seq`.
Subscriber устанавливает `my_last_seq = initial_packet_seq - 1`, так что
первый `next_packet` вернёт keyframe (decoder может start без glitches).
**Risk:** если в момент handshake **last_keyframe_seq уже выехал из
ring** (slow start subscriber, GOP > ring_slots packets) — subscriber
detect overrun в первом read и переходит на следующий keyframe.
В implementation `publisher_publish_packet` для оптимизации может маркировать
slot перед IDR как **persistent** (флаг в reserved), но **v0.2 keep simple**
просто требуем что `packet_ring_slots × avg_packet_size > GOP_size_in_bytes`
для нормальной работы. Sizing guide см. в [docs/integration.md](integration.md).
### 10.15 Error codes (новые)
| Code | Name | Когда |
|---|---|---|
| -20 | `CUFRAMES_ERR_PACKET_OVERSIZED` | publish_packet с size > max_packet_size |
| -21 | `CUFRAMES_ERR_NO_PACKET_RING` | subscriber запросил packets, publisher без packet ring |
| -22 | `CUFRAMES_ERR_NO_CODEC_PARAMS` | get_codec_params вызван до set_codec_extradata publisher'ом |
| -23 | `CUFRAMES_ERR_PACKET_OVERRUN` | subscriber slow — packet seq уехал, надо resync на keyframe |
### 10.16 Open для v0.3+
- **Sub-stream selection** — publisher может публиковать несколько
packet rings (для multi-resolution streams). Сейчас один key = один stream.
v0.3 → `<key>-substream-<N>` naming?
- **Codec change mid-stream** — текущий design требует publisher restart.
Future: invalidate codec_extradata + bump generation field.
- **Audio streams** — analogichno в packet ring, но codec_id = audio (AAC,
Opus). v0.3.
+284
View File
@@ -0,0 +1,284 @@
# cuframes Python bindings
Status: **v0.4 — Phase 0 alpha** (issue [gx/cuframes#6](http://server:3000/gx/cuframes/issues/6))
Python пакет `cuframes` — pybind11-обёртка над C ABI libcuframes. Цель —
позволить downstream ML/CV пайплайнам (yolo-world-detector, zone-motion,
custom скриптам) подписываться на cuframes **без CPU round-trip**: получать
NV12 frames прямо как CUDA pointer / `torch.Tensor` (DLPack export, zero-copy
из VRAM publisher'а в VRAM consumer'а).
## Установка
Standalone wheel (рекомендуемый):
```bash
cd cuframes/python/
pip install -e . --no-build-isolation
```
Через корневой CMake:
```bash
cmake -B build -DBUILD_PYTHON_BINDINGS=ON
cmake --build build -j
```
## Quick start
```python
import cuframes
print(cuframes.version_string()) # "0.4.0"
with cuframes.subscribe("cam-parking",
consumer_name="yolo-world",
connect_timeout_ms=5000) as sub:
with sub.next_frame(timeout_ms=1000) as frame:
print(f"{frame.width}x{frame.height} "
f"format={frame.format} seq={frame.seq}")
```
## API
### `cuframes.subscribe(key, ...)`
Создать подписку на publisher. Возвращает `CuframesSubscriber`.
| Параметр | Тип | Default | Назначение |
|---|---|---|---|
| `key` | `str` | (required) | Имя publisher'а (`"cam-parking"` и т.п.) |
| `consumer_name` | `str \| None` | `None` (auto-generated) | Идентификатор подписки |
| `mode` | `SubscriberMode` | `NEWEST_ONLY` | `NEWEST_ONLY` skip'ит промежуточные frames, `STRICT_ORDER` — все по порядку |
| `cuda_device` | `int` | `0` | CUDA device id |
| `connect_timeout_ms` | `int` | `-1` (бесконечно) | Сколько ждать publisher'а |
| `consumer_stream` | `int` | `0` (default stream) | `cudaStream_t` как pointer |
### `CuframesSubscriber`
Контекст-менеджер. Methods/properties:
```python
sub.next_frame(timeout_ms=-1) # → CuframesFrame
sub.close() # idempotent
# read-only properties
sub.key # str
sub.consumer_name # str
sub.mode # SubscriberMode
sub.cuda_device # int
sub.consumer_stream # int (cudaStream_t ptr)
sub.closed # bool
# health / stats (Phase 0 counters)
sub.frames_received # int
sub.timeouts # int
sub.errors # int
sub.last_seq # int (sequence number последнего frame'а)
sub.gap_count # int (proxy для drop count в NEWEST_ONLY)
sub.last_frame_pts_ns # int
sub.stats() # dict — snapshot всех counters для MQTT publish
```
### `CuframesFrame`
Контекст-менеджер. Properties (read-only):
```python
frame.cuda_ptr # int (uintptr_t)
frame.format # PixelFormat
frame.width # int
frame.height # int
frame.pitch_y # int — pitch Y plane (важно — может быть > width!)
frame.pitch_uv # int
frame.seq # int — sequence number у publisher'а
frame.pts_ns # int — CLOCK_MONOTONIC у publisher'а
frame.released # bool
# DLPack export (zero-copy)
frame.dlpack_y() # capsule — Y plane как 2D uint8 GPU tensor
frame.dlpack_uv() # capsule — UV plane (только NV12)
frame.__dlpack__() # protocol для torch.from_dlpack(frame)
frame.__dlpack_device__() # (kDLCUDA=2, device_id)
```
## Интеграция с PyTorch
```python
import torch
import cuframes
with cuframes.subscribe("cam-parking", connect_timeout_ms=5000) as sub:
with sub.next_frame() as frame:
# Single-plane (default — Y plane для NV12)
y_tensor = torch.from_dlpack(frame)
# Multi-plane explicit
y = torch.from_dlpack(frame.dlpack_y()) # shape=[H, W] uint8
uv = torch.from_dlpack(frame.dlpack_uv()) # shape=[H/2, W] uint8
# Y plane уже в VRAM — никаких copy. Можно сразу feed в NN.
y_float = y.float() / 255.0 # будет на CUDA device
```
## Интеграция с CuPy
```python
import cupy
import cuframes
with cuframes.subscribe("cam-parking", connect_timeout_ms=5000) as sub:
with sub.next_frame() as frame:
y_array = cupy.from_dlpack(frame.dlpack_y()) # cupy.ndarray на GPU
```
## Pattern: reconnect-loop для долгоживущего consumer'а
```python
import time
import cuframes
def consume_camera(key: str, on_frame):
while True:
try:
with cuframes.subscribe(key, connect_timeout_ms=5000) as sub:
while True:
try:
with sub.next_frame(timeout_ms=1000) as frame:
on_frame(frame)
except cuframes.CuframesFrameTimeout:
# просто нет новых кадров — продолжаем ждать
continue
except cuframes.CuframesPublisherGone:
# publisher умер / перезапускается — переподписываемся
print(f"publisher {key} gone, reconnect через 1s")
time.sleep(1)
except cuframes.CuframesError as e:
# фатальная ошибка — логируем и продолжаем
print(f"error: {e!r}")
time.sleep(5)
```
## Per-subscriber CUDA stream
В продакшене на 4+ камеры каждый subscriber должен иметь свой stream —
иначе `cudaStreamWaitEvent` сериализует всех consumer'ов через default
stream.
С `cuda-python`:
```python
from cuda import cudart
import cuframes
err, stream = cudart.cudaStreamCreate()
assert err == cudart.cudaError_t.cudaSuccess
with cuframes.subscribe("cam-parking", consumer_stream=int(stream)) as sub:
...
```
С `torch.cuda.Stream`:
```python
import torch
import cuframes
stream = torch.cuda.Stream()
with cuframes.subscribe("cam-parking",
consumer_stream=stream.cuda_stream) as sub:
with torch.cuda.stream(stream):
with sub.next_frame() as frame:
tensor = torch.from_dlpack(frame)
# ... inference на этом stream'е ...
```
## Pitch alignment — важно!
NVDEC отдаёт NV12 с pitch alignment 256 байт. Для камер с шириной не
кратной 256 (`gate_lpr 2688×1520` → pitch 2688 OK; но представьте `640×480`
→ pitch обычно 640 байт, но **может быть 768**).
```python
# WRONG — assume pitch == width
y = torch.frombuffer(...) # данные смещены
# RIGHT — использовать DLPack который сам respect'ит strides
y = torch.from_dlpack(frame.dlpack_y()) # stride учтён правильно
# ALTERNATIVELY — manual через cuda-python с правильным pitch
ptr = frame.cuda_ptr
pitch = frame.pitch_y
height = frame.height
```
## Thread-safety contract
- Каждый `CuframesSubscriber` принадлежит **одному Python потоку**.
Создание и все вызовы (`next_frame`, `close`) — в одном thread.
- Несколько subscriber'ов в разных потоках — **OK** (каждому свой handle,
свой CUDA stream).
- `CuframesFrame` тоже принадлежит одному потоку — после `release()` его
CUDA pointer становится недействительным, доступ из другого потока —
undefined behavior.
- Внутренний GIL отпускается на блокирующих вызовах (`subscriber_create`,
`next_frame`) — другие Python потоки могут выполняться.
Для multi-camera в одном процессе используйте `asyncio` или `threading`:
```python
import threading
import cuframes
def worker(camera_key):
with cuframes.subscribe(camera_key, connect_timeout_ms=5000) as sub:
# subscribe в этом же потоке
while True:
with sub.next_frame(timeout_ms=1000) as frame:
process(frame)
for key in ["cam-parking", "cam-front_yard", "cam-gate_lpr", "cam-back_yard"]:
threading.Thread(target=worker, args=(key,), daemon=True).start()
```
## Error taxonomy
Все exception'ы наследуются от `CuframesError`. Конкретные subclass'ы
позволяют разную обработку:
| Exception | Когда выбрасывается | Что делать |
|---|---|---|
| `CuframesPublisherGone` | publisher умер или ещё не стартовал | reconnect-loop |
| `CuframesFrameTimeout` | timeout без frame'а | продолжать ждать или log'нуть |
| `CuframesDeviceLost` | CUDA error на cross-process sync | abort, не recoverable |
| `CuframesShmError` | socket/mmap/IPC error | log, abort или восстановить |
| `CuframesProtocolMismatch` | версия libcuframes несовместима | пересобрать |
| `CuframesInvalidArgument` | bug в caller | fix code |
| `CuframesOutOfMemory` | cudaMalloc fail | reduce работу |
| `CuframesInternal` | bug в libcuframes | report |
## Backpressure
`next_frame()` blocking call с GIL released. Если consumer медленнее
publisher'а:
- В `NEWEST_ONLY` mode (default) — publisher продолжает писать, consumer
получает **самый свежий** frame (промежуточные пропускает). `gap_count`
растёт.
- В `STRICT_ORDER` mode — при ring overflow `CuframesPublisherGone`
reconnect.
Frame удерживать долго **нельзя**: в `STRICT_WAIT` policy publisher
заблокирует ring. Pattern — забрать DLPack, инициировать GPU работу,
release frame сразу.
## Текущие ограничения (Phase 0)
- Publisher API не обёрнут (только subscriber-side)
- Packet ring (encoded video) не обёрнут
- Async callback API не обёрнут
- `ring_occupancy` / реальный drop count — нет в C API (counted в pybind как
`gap_count`, это proxy)
- Smoke test реального subscribe требует Docker IPC namespace (cuframes
socket/SHM живут в namespace publisher'а)
Эти ограничения снимаются по мере необходимости — issues в
[gx/cuframes](http://server:3000/gx/cuframes).
+33 -1
View File
@@ -181,4 +181,36 @@ Phase 0 PoC (2026-05-14):
- **Docker:** 29.1.3 с nvidia-container-runtime
- **Container:** Ubuntu 24.04 + GCC 13 + Clang + CMake 3.28 + Ninja
Дополнительный target matrix будет в CI после Phase 4.
## Production deployment matrix (v0.1.0)
Что подтверждено в 24h+ production run:
| Слой | Версия | Comments |
|---|---|---|
| NVIDIA driver | 555+ | минимум для CUDA 12 user runtime |
| CUDA toolkit (build) | 12.4 (Debian 12 / Ubuntu 22.04) либо 13.0 (Ubuntu 24.04) | toolkit для builder image, не runtime |
| GPU | RTX 5090 (sm_120) | проверено; раньше — sm_75 минимум |
| Builder OS | Ubuntu 22.04 (glibc 2.35) | forward-compat с Debian 12 runtime |
| Runtime OS (Frigate) | Debian 12 (glibc 2.36) | base image Frigate `stable-tensorrt` |
| Runtime OS (cctv-backend) | Ubuntu 22.04 либо Debian 12 | matched с builder |
| Docker | 29.1.x | для buildx |
| docker buildx | v0.34.0+ | `apt install docker-buildx-plugin` либо manual install из GH releases |
| nvidia-container-toolkit | 1.14+ | для `runtime: nvidia` |
## Docker namespace requirements (cross-container CUDA IPC)
Для consumer'а который подключается к publisher'у в **другом** container'е:
| Что нужно | Как настроить |
|---|---|
| `/dev/shm` shared (header + ring metadata) | `ipc: container:<publisher>` либо `ipc: shareable` у publisher + same у consumer |
| `/proc` visibility (CUDA IPC peer validation) | `pid: container:<publisher>` |
| `/run/cuframes/*.sock` доступен | volume mount `cuframes_sock:/run/cuframes:ro` |
| GPU access | `runtime: nvidia` + `NVIDIA_VISIBLE_DEVICES=all` |
| Socket file permissions | `user: root` либо chmod в publisher |
**Все 5** должны быть выполнены. Подробности — [docs/troubleshooting.md](troubleshooting.md).
**Special case: s6-overlay containers (Frigate, linuxserver.io stack)**: `pid:` share **невозможен** — s6-overlay требует PID 1. Workaround: только `ipc:` + race window connect. См. troubleshooting.
Дополнительный target matrix будет в CI после Phase 4 (см. [ROADMAP.md](../ROADMAP.md)).
+326
View File
@@ -0,0 +1,326 @@
# Troubleshooting
Реальные грабли которые мы прошли при первой production deployment'е cuframes
(Frigate + custom C++ processor + custom Python). Документировано чтобы вы их
не повторяли.
## Содержание
- [Runtime / CUDA IPC](#runtime--cuda-ipc)
- [`cudaIpcOpenEventHandle: invalid device context`](#cudaipcopeneventhandle-invalid-device-context)
- [Subscriber timeout (`cuframes_subscriber_create: timeout`)](#subscriber-timeout)
- [Permission denied на socket](#permission-denied-на-socket)
- [Frigate-specific](#frigate-specific)
- [`s6-overlay-suexec: fatal: can only run as pid 1`](#s6-overlay-suexec-fatal-can-only-run-as-pid-1)
- [`No such filter: 'scale_cuda'`](#no-such-filter-scale_cuda)
- [Missing dynamic .so после ffmpeg replace](#missing-dynamic-so-после-ffmpeg-replace)
- [Build / FFmpeg patch](#build--ffmpeg-patch)
- [`libcuframes not found` при configure](#libcuframes-not-found-при-configure)
- [`ffbuild/library.mak: No such file`](#ffbuildlibrarymak-no-such-file)
- [`could not find a working compiler` (GMP)](#could-not-find-a-working-compiler-gmp)
- [`zlib: download failed` в crosstool-NG](#zlib-download-failed-в-crosstool-ng)
- [`stdbit.h: No such file` при `--enable-cuda-llvm`](#stdbith-no-such-file-при---enable-cuda-llvm)
- [Docker / IPC](#docker--ipc)
- [Cross-container CUDA IPC: ipc + pid namespace share](#cross-container-cuda-ipc-ipc--pid-namespace-share)
- [Buildx container driver не видит host images](#buildx-container-driver-не-видит-host-images)
- [Networking / RTSP](#networking--rtsp)
- [RTSP/RTP UDP не доходит до клиента (docker NAT)](#rtsprtp-udp-не-доходит-до-клиента-docker-nat)
- [`Nonmatching transport in server reply`](#nonmatching-transport-in-server-reply)
- [Gitea Actions / CI](#gitea-actions--ci)
- [`node: executable file not found`](#node-executable-file-not-found)
- [`SyntaxError: Unexpected token '{'` (Node 12)](#syntaxerror-unexpected-token--node-12)
---
## Runtime / CUDA IPC
### `cudaIpcOpenEventHandle: invalid device context`
**Симптом**: subscriber сразу после `cuframes_subscriber_create` падает с этой ошибкой.
**Причина**: CUDA driver проверяет IPC peer через `/proc/<pid>/...`. Если процесс publisher'а **не виден** в PID namespace consumer'а — context считается невалидным.
**Fix**: shared PID namespace.
Docker:
```yaml
consumer:
ipc: "container:<publisher>" # shared /dev/shm
pid: "container:<publisher>" # ← вот это критично, без него fail
```
Host process: запуск consumer'а на host'е (либо publisher'а на host'е тоже) — same default namespace.
**Caveat**: если consumer image использует s6-overlay (Frigate, linuxserver.io
images) — `pid: container:` несовместим (см. [соответствующую секцию](#s6-overlay-suexec-fatal-can-only-run-as-pid-1)).
### Subscriber timeout
**Симптом**: `cuframes_subscriber_create: timeout` без других ошибок.
**Причины** (в порядке вероятности):
1. `/run/cuframes/<key>.sock` не виден consumer'у — забыли volume-mount
2. `/run/cuframes` смонтирован, но publisher ещё не успел создать socket — увеличить `connect_timeout_ms`
3. Publisher запущен, socket есть, но **permission denied** — см. ниже
### Permission denied на socket
**Симптом**: socket виден через `ls -la /run/cuframes/`, owner `root`. Consumer process — non-root user → не может `connect()`.
**Fix**:
- Запустить consumer как root: `user: root` в compose
- Либо изменить permissions socket после создания (publisher delegation) — TBD в v0.2
---
## Frigate-specific
### `s6-overlay-suexec: fatal: can only run as pid 1`
**Симптом**: container Frigate'а в restart loop, в logs только эта ошибка.
**Причина**: `pid: container:<publisher>` сделал Frigate not-PID-1 в shared namespace. s6-overlay v3 strictly требует PID 1 для proper signal handling/zombie reaping.
**Fix**: убрать `pid: container:` для Frigate. Только `ipc: container:` shared.
**Trade-off**: без shared pid некоторые edge cases CUDA IPC ломаются (см. [соответствующую секцию](#cudaipcopeneventhandle-invalid-device-context)). Frigate **на практике** работает потому что подключается до того как CUDA driver проверяет peer (race window race), но если publisher restart'нётся посередине — Frigate'у не удастся пере-подключиться без перезапуска.
**Real fix** (planned v0.2): encoded packet sharing — Frigate detect получает кадры через decoded path (work-around), record получает encoded через socket-based protocol который **не** требует cudaIpcOpenEventHandle.
### `No such filter: 'scale_cuda'`
**Симптом**: Frigate ffmpeg subprocess падает с этой ошибкой в `AVFilterGraph`.
**Причина**: наш patched FFmpeg собран без `--enable-cuda-llvm` (см. [stdbit.h grабля](#stdbith-no-such-file-при---enable-cuda-llvm)). Без cuda-llvm в FFmpeg нет CUDA filters (scale_cuda, overlay_cuda).
**Fix**: в Frigate config.yml явно отключи hwaccel cuda:
```yaml
ffmpeg:
hwaccel_args: [] # CPU scale вместо scale_cuda
```
Cost: 5-10% CPU per FHD25 камера. **Real fix** (v0.2): publisher-side resize в cuframes сам.
### Missing dynamic .so после ffmpeg replace
**Симптом**: после `docker cp` patched ffmpeg в Frigate container — `ldd ffmpeg`
показывает `libharfbuzz.so.0 => not found`, `libfribidi.so.0 => not found`, …
~20 missing .so.
**Причина**: Frigate's bundled ffmpeg **статически слинкован** (NickM-27/FFmpeg-Builds
делает full static build). Все 30+ deps встроены в один binary. Frigate runtime
image **не имеет** этих .so packages installed (ему не надо — bundled ffmpeg
self-contained).
Наш custom ffmpeg — **dynamic linked** (apt deps). Нужны .so на target.
**Fix**: либо
- `apt install` missing libs в Frigate (additive image modification):
```bash
apt install libharfbuzz0b libfribidi0 librist4 libsrt1.5-openssl libssh-4 \
libvpx7 libwebpmux3 libwebp7 libdav1d6 libaom3 libmp3lame0 \
libsvtav1enc1 libtheora0 libvorbis0a libvorbisenc2 \
libx264-164 libx265-199 libopus0
```
- Либо строить наш ffmpeg static (sources from NickM-27 pipeline) — complex
(см. [zlib download / GMP compiler граблю](#zlib-download-failed-в-crosstool-ng))
Best practice: создать `Dockerfile.frigate` overlay поверх Frigate image,
который добавляет deps и копирует ffmpeg. Запечь в image, не in-place patch.
---
## Build / FFmpeg patch
### `libcuframes not found` при configure
**Симптом**: FFmpeg configure (с `--enable-libcuframes`) fails с этой ошибкой
из `enabled libcuframes && require libcuframes ...`. config.log показывает
`fatal error: cuframes/cuframes.h: No such file or directory`.
**Причины**:
1. **CMake install rules отсутствовали** в libcuframes (early commits до 601806a).
`cmake --install` создавал пустой prefix. Fix: обновить cuframes до ≥ 601806a.
2. **Wrong HINTS в find_library**: твой проект ищет в `${CUFRAMES_ROOT}/build/...`
но install layout кладёт в `${CUFRAMES_ROOT}/lib`. Добавь оба пути в HINTS.
3. **`rm -f libcuframes.so*`** удалил .so но **.a** file называется
`libcuframes_static.a` (не `libcuframes.a`) → linker не находит `-lcuframes`.
Fix: либо не удаляй .so, либо переименуй .a при install.
### `ffbuild/library.mak: No such file`
**Симптом**: configure FFmpeg success, но `make` падает сразу:
`Makefile:123: ffbuild/library.mak: No such file or directory`.
**Причина**: вы сделали ваш fork FFmpeg через snapshot (не git clone), и **случайно
исключили `ffbuild/`** в rsync. Это **source files** FFmpeg, не build artifacts.
**Fix**: убедись что `ffbuild/` есть в твоём FFmpeg checkout (`ls ffbuild/library.mak`).
Если делаешь snapshot через rsync — не используй `--exclude=ffbuild`.
### `could not find a working compiler` (GMP)
**Симптом**: crosstool-NG build падает на `Installing GMP for host` с
`configure: error: could not find a working compiler`. config.log показывает
`no, long long reliability test 1`.
**Причина**: GMP 6.2.1 имеет known issue с GCC 11+ (Ubuntu 22.04 default).
Проверка long-long reliability fail'ит false-positive.
**Fix**: pin GMP к 6.3.0 в `ct-ng-config`:
```
CT_GMP_V_6_3=y
# CT_GMP_V_6_2 is not set
CT_GMP_VERSION="6.3.0"
```
И убедись что crosstool-NG version (commit) поддерживает 6.3.0 (≥ master 2024-09).
### `zlib: download failed` в crosstool-NG
**Симптом**: crosstool-NG step `Retrieving 'zlib-1.2.12'` fail'ит.
**Причина**: zlib.net убрали старые versions с дефолтного location — теперь они
только в `/fossils/` subdirectory. Crosstool-NG hardcoded URL не работает.
**Fix**: pre-fetch tarball + положить в local cache:
```bash
wget https://zlib.net/fossils/zlib-1.2.12.tar.gz -O preload/zlib-1.2.12.tar.gz
```
В Dockerfile перед `ct-ng build`:
```dockerfile
COPY preload/*.tar.gz /root/src/
```
`CT_LOCAL_TARBALLS_DIR=${HOME}/src` — crosstool-NG найдёт в cache и не пойдёт
download.
### `stdbit.h: No such file` при `--enable-cuda-llvm`
**Симптом**: FFmpeg configure с `--enable-cuda-llvm` fail'ит:
`fatal error: stdbit.h: No such file or directory`. ERROR: cuda_llvm requested
but not found.
**Причина**: `stdbit.h` — C23 standard header. Доступен в glibc ≥ 2.38.
- Ubuntu 22.04 = glibc 2.35 — **нет**
- Debian 12 = glibc 2.36 — **нет**
- Ubuntu 24.04 = glibc 2.39 — есть
- Debian 13 (trixie) = glibc 2.38+ — есть
**Fix options**:
1. Build на newer base (Ubuntu 24.04+). Но runtime target (Frigate Debian 12)
не запустит binary с glibc-2.38 symbols (backwards-incompatible).
2. Убрать `--enable-cuda-llvm`. Потеря: CUDA filters (`scale_cuda`, `overlay_cuda`,
`hwupload_cuda`). Decode/encode через NVDEC/NVENC всё равно работают.
3. Дождаться когда Frigate base обновится до newer Debian — вне твоего контроля.
**На практике**: убираем cuda-llvm, в Frigate config `hwaccel_args: []`.
См. [scale_cuda секцию](#no-such-filter-scale_cuda).
---
## Docker / IPC
### Cross-container CUDA IPC: ipc + pid namespace share
| Что нужно | Compose option |
|---|---|
| /dev/shm shared (для cuframes header + SHM ring) | `ipc: container:<publisher>` (либо `ipc: shareable` у publisher + same у consumer) |
| /proc visibility (для CUDA IPC peer validation) | `pid: container:<publisher>` |
| `/run/cuframes/*.sock` доступен | volume mount: `cuframes_sock:/run/cuframes:ro` |
| GPU access | `runtime: nvidia` |
| Socket permissions | `user: root` (либо chmod socket в publisher) |
**Все 5** должны быть выполнены. Один пропуск — fail при subscriber_create или
cudaIpcOpenEventHandle.
### Buildx container driver не видит host images
**Симптом**: при использовании custom buildx builder (`docker buildx create
--driver docker-container ...`) с `FROM local-image:tag` — error `failed to
authorize: 403 Forbidden` (buildkit пытается pull с registry).
**Причина**: container driver buildx изолирован, не имеет доступа к host's
local docker daemon images. Pull через registry.
**Fix**: либо
- Не использовать custom builder — `docker buildx use default` (использует host
daemon). Минус: теряем `--cache-to/--cache-from type=local`.
- Либо push local image в **registry** (local или gitea), и buildx pull'ит оттуда.
---
## Networking / RTSP
### RTSP/RTP UDP не доходит до клиента (docker NAT)
**Симптом**: RTSP server в docker контейнере с `ports: "554:8555"`. Клиент (TV, VLC)
делает RTSP SETUP successfully (TCP control работает), но video frames не приходят.
**Причина**: RTP идёт **UDP**, sourced из docker network namespace. SNAT MASQUERADE
для outbound работает, но RTP destination port (которое клиент опубликовал в SETUP)
**не маппится обратно** через docker bridge — клиент видит UDP packets от чужого
source IP (docker network 172.x), не от 192.168.88.23 как expected.
**Fix**: `network_mode: host` для RTSP-server контейнера. Тогда server listens
**напрямую** на host interfaces, RTP packets идут без NAT.
Trade-offs:
- Все ports app'а listen на host network (нет port mapping). Проверь port collisions.
- DB env vars (postgres:5432 в docker network DNS) надо менять на host paths
(`localhost:5433` если postgres exposed на host port 5433).
### `Nonmatching transport in server reply`
**Симптом**: `ffprobe -rtsp_transport tcp -i rtsp://...` falls с этим сообщением.
**Причина**: RTSP server возвращает SDP с UDP-only transport. Client ожидает TCP
interleaved.
**Fix**: использовать UDP transport: `-rtsp_transport udp` (либо default behavior).
Если TV не поддерживает UDP — нужен RTSP server который умеет RTP-over-TCP
interleaved (cctv-processor v0.1 не умеет).
---
## Gitea Actions / CI
### `node: executable file not found`
**Симптом**: первый JS action (например `actions/checkout@v4`) fail'ит:
`OCI runtime exec failed: exec: "node": executable file not found in $PATH`.
**Причина**: гитея act_runner запускает JS actions через `node`, но твой
custom container (например `nvidia/cuda:...`) не имеет node installed.
**Fix**: pre-install node в первом `run:` step (до actions/checkout):
```yaml
steps:
- name: Bootstrap node
run: apt-get update && apt-get install -y nodejs git ca-certificates
- name: Checkout
uses: actions/checkout@v4
```
Либо использовать container с node pre-installed (`docker.gitea.com/runner-images:ubuntu-22.04`).
### `SyntaxError: Unexpected token '{'` (Node 12)
**Симптом**: после `apt install nodejs` в Ubuntu 22.04 — actions/checkout@v4 fail'ит:
`SyntaxError: Unexpected token '{' at static {...}`.
**Причина**: Ubuntu 22.04 apt'овский `nodejs` = Node **12**. `actions/checkout@v4`
скомпилирован для Node 20+ (static class blocks — ES2022).
**Fix**: install Node 20 from NodeSource:
```bash
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
```
В Ubuntu 24.04 apt уже даёт Node 20 — там goes автоматически.
+8
View File
@@ -0,0 +1,8 @@
# Скопировать в .env (не commit'ить!)
# .env должен быть в .gitignore
# Камеры: пароли admin user'а на Dahua/Hikvision/etc
CAM_PARKING_PASS=changeme
# Frigate API/UI auth password
FRIGATE_RTSP_PASSWORD=changeme
+61
View File
@@ -0,0 +1,61 @@
# examples/frigate-compose
Reference docker-compose для Frigate + cuframes integration. **НЕ** копировать
в production бездумно — это шаблон, адаптируй под свою инфру (IP-адреса камер,
пароли, mount paths, network).
## Quickstart
1. Build patched Frigate image (single-time setup, ~15 мин):
```bash
# См. docs/integrations/frigate.md, Шаг 1 — там полный Dockerfile.
docker build -t local/frigate-cuframes:latest -f Dockerfile.frigate .
```
2. Pull cuframes publisher image:
```bash
docker pull git.goldix.org/gx/cuframes:0.1
# либо собрать local: docker build -t local/cuframes:0.1 -f docker/Dockerfile.runtime ../..
```
3. Скопировать .env:
```bash
cp .env.example .env
$EDITOR .env # подставь свои camera passwords
```
4. Адаптировать `docker-compose.yml`:
- `parking-cam-ip` → реальный IP камеры
- `--key cam-parking` → имя по вкусу (должно matche'ить config.yml `cuframes://<key>`)
- `cam-parking` в Frigate config → так же matched
5. Адаптировать `config/config.yml`:
- детектор (cpu / onnx / tensorrt)
- пути к media
- дополнительные камеры если нужно
6. Run:
```bash
docker compose up -d
docker logs -f frigate
# UI: http://localhost:5000 (internal) либо https://localhost:8971 (auth)
```
## Что демонстрирует
- Один publisher (`cuframes-pub-parking`) делает 1× NVDEC на parking-камеру
- Frigate подключается к publisher через `ipc:container:` + `cuframes://` URL
- Frigate **не** делает свой NVDEC для detect-path — берёт готовые NV12 frames
## Что НЕ демонстрирует
- Record path — Frigate всё ещё открывает второй RTSP к камере (для архива
`-c:v copy` mux). v0.2 cuframes решит через encoded packet sharing
(см. [issue #2](https://git.goldix.org/gx/cuframes/issues/2))
- Multi-camera setup — добавь больше publisher'ов и camera-blocks в config.yml
- HA/MQTT интеграция — добавь свой mqtt block
## См. также
- [docs/integrations/frigate.md](../../docs/integrations/frigate.md) — полный walkthrough
- [docs/integration.md](../../docs/integration.md) — общая интеграция
@@ -0,0 +1,49 @@
# Minimal Frigate config с cuframes integration.
# Полный guide: docs/integrations/frigate.md
mqtt:
enabled: false
detectors:
# Замени на свой detector (tensorrt / onnx / cpu). Здесь — placeholder.
cpu:
type: cpu
# CRITICAL: hwaccel cuda отключён — наш patched ffmpeg без --enable-cuda-llvm
# (не работает на glibc < 2.38 что у Debian 12, на котором Frigate runtime).
# Без cuda-llvm нет scale_cuda filter. Detect-path использует CPU scale, но
# decode уже сделан у publisher'а — net выигрыш всё равно.
ffmpeg:
hwaccel_args: []
output_args:
record: preset-record-generic-audio-aac
cameras:
parking_overview:
enabled: true
friendly_name: Парковка
ffmpeg:
inputs:
# main (full-res) — только запись в архив через прямой RTSP (`-c:v copy`, no decode у Frigate)
# После cuframes v0.2 этот path тоже может через cuframes_packets:// (encoded share)
- path: rtsp://admin:${FRIGATE_RTSP_PASSWORD}@parking-cam-ip:554/cam/realmonitor?channel=1&subtype=0
roles: [record]
# sub-stream → через cuframes (decoded у publisher'а, без второго NVDEC)
- path: cuframes://cam-parking
input_args: -f cuframes
roles: [detect]
detect:
width: 640
height: 480
fps: 5
record:
enabled: true
retain:
days: 7
snapshots:
enabled: true
retain:
default: 7
@@ -0,0 +1,73 @@
# Reference docker-compose для Frigate + cuframes integration.
# Полный guide: docs/integrations/frigate.md
#
# Что нужно подготовить заранее:
# 1. Build local image local/frigate-cuframes:latest по Dockerfile.frigate
# (см. docs/integrations/frigate.md, Шаг 1)
# 2. Pull cuframes runtime image:
# docker pull git.goldix.org/gx/cuframes:0.1 # либо собрать local
# 3. Скопировать config/config.yml (placeholder в config/ рядом)
# 4. .env с CAM_PARKING_PASS=... и FRIGATE_RTSP_PASSWORD=...
#
# Запуск:
# docker compose up -d
# # UI: http://host:5000 (internal, без auth) либо https://host:8971 (with auth)
services:
# 1× publisher на камеру — single source of RTSP + NVDEC
cuframes-pub-parking:
image: git.goldix.org/gx/cuframes:0.1
container_name: cuframes-pub-parking
restart: unless-stopped
runtime: nvidia
ipc: shareable
shm_size: 256m
environment:
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
volumes:
- cuframes_sock:/run/cuframes
command:
- /usr/local/bin/cuframes-rtsp-source
- --rtsp
# Используем sub-stream для detect-path (lighter resolution, тот же camera load)
- "rtsp://admin:${CAM_PARKING_PASS}@parking-cam-ip:554/cam/realmonitor?channel=1&subtype=1"
- --key
- cam-parking
- --ring
- "6"
- --verbose
frigate:
image: local/frigate-cuframes:latest # см. docs/integrations/frigate.md Шаг 1
container_name: frigate
restart: unless-stopped
depends_on:
cuframes-pub-parking:
condition: service_started
runtime: nvidia
privileged: true
shm_size: 512m
# WARN: только ipc share — pid НЕ shared (Frigate's s6-overlay требует PID 1).
# Frigate подсоединяется к first CUDA context publisher'а в shared /dev/shm.
ipc: "container:cuframes-pub-parking"
environment:
FRIGATE_RTSP_PASSWORD: "${FRIGATE_RTSP_PASSWORD}"
NVIDIA_VISIBLE_DEVICES: all
NVIDIA_DRIVER_CAPABILITIES: compute,video,utility
ports:
- "5000:5000" # UI без auth (internal, не expose external!)
- "8971:8971" # UI с HTTPS + auth
- "8554:8554" # RTSP restream (go2rtc)
- "8555:8555/tcp"
- "8555:8555/udp"
volumes:
- cuframes_sock:/run/cuframes:ro
- ./config/config.yml:/config/config.yml:ro
- ./media:/media/frigate
- type: tmpfs
target: /tmp/cache
tmpfs: { size: 1000000000 }
volumes:
cuframes_sock:
+78
View File
@@ -0,0 +1,78 @@
# examples/python-consumer
Reference Python consumer для cuframes через `ctypes` wrapper.
## Use case
AI/ML pipeline (PyTorch / ONNX / TensorRT) которому нужны декодированные кадры
с камер. Без cuframes — каждый Python скрипт открывает RTSP + decode сам.
С cuframes — подписывается на готовые NV12 frames от publisher'а.
## Запуск
```bash
# Publisher должен быть запущен (см. tools/cuframes-rtsp-source или Docker image)
cuframes-rtsp-source --rtsp rtsp://admin:pw@cam-ip:554/... --key cam-parking &
# Consumer (same host, либо same docker namespace — см. требования ниже)
python3 cuframes_consumer.py --key cam-parking --max-frames 100
```
Ожидаемый output:
```
[consumer] connected to 'cam-parking'
[consumer] first frame: 640x480 NV12, pitch_y=640, pitch_uv=640, cuda_ptr=0x...
[consumer] received=25 seq=42 pts_ms=...
...
=== RESULT ===
received: 100 / 100
elapsed: 3.96s
avg_fps: 25.03
```
## Что этот пример НЕ делает
- **НЕ копирует** GPU NV12 frame на host — `cuda_ptr` это raw CUDA device pointer.
Для реальной работы нужно:
- `pycuda` / `cupy` / `cuda-python` библиотека для CUDA memcpy
- либо передать `cuda_ptr` напрямую в GPU-aware ML framework (PyTorch's
`torch.cuda.IntTensor.from_dlpack` etc.)
- **НЕ конвертирует** NV12 → RGB. Используй `cv2.cvtColor(nv12, cv2.COLOR_YUV2RGB_NV12)`
на host или GPU-side conversion.
- **НЕ обрабатывает** inference — это skeleton, в твоём pipeline replace
comment-block `### ВАШ ML PIPELINE ЗДЕСЬ ###` с актуальным кодом.
## Требования
| | Значение |
|---|---|
| Python | 3.8+ |
| `libcuframes.so.0` | в `LD_LIBRARY_PATH` (либо `/usr/local/lib`) |
| Publisher running | да, с matching `--key` |
| Same IPC namespace | да (host либо `ipc:container:<publisher>` в docker) |
| Same PID namespace | да (host либо `pid:container:<publisher>` в docker) |
| NVIDIA GPU + driver | для access `cuda_ptr` (read-only frame от publisher'а) |
## Docker-style
```yaml
# В compose рядом с publisher service
ai-pipeline:
image: your-ai-image:cuda
runtime: nvidia
ipc: "container:cuframes-pub-parking"
pid: "container:cuframes-pub-parking"
volumes:
- cuframes_sock:/run/cuframes:ro
environment:
LD_LIBRARY_PATH: /usr/local/lib
command: python3 /app/cuframes_consumer.py --key cam-parking --max-frames 1000000
```
## v0.3 → first-class pybind11 bindings
Текущий ctypes pattern будет заменён на native pybind11 bindings в v0.3 cuframes
([ROADMAP.md](../../ROADMAP.md)). Тогда API будет более pythonic + zero-copy через
`__cuda_array_interface__` / `dlpack`.
@@ -0,0 +1,206 @@
#!/usr/bin/env python3
"""
Reference Python consumer для cuframes (через ctypes wrapper).
До v0.3 (когда появятся первоклассные pybind11 bindings) — это minimal
working pattern для AI/ML скриптов которые хотят подписаться на cuframes IPC.
Pattern:
1. subscribe to cuframes (open libcuframes.so via ctypes)
2. в цикле: получить next() frame
3. cudaMemcpy → host (через pycuda либо отдельной CUDA-Python библиотекой)
4. передать в свой ML pipeline (ONNX/TensorRT/PyTorch)
5. release frame обратно publisher'у
Limitations:
- Этот skeleton НЕ делает actual CUDA copy (нужна pycuda / cupy / cuda-python)
- Только sync API
- Только NV12 (v0.1)
Запуск:
python3 cuframes_consumer.py --key cam-parking --max-frames 100
Требования (на target host):
- libcuframes.so в LD_LIBRARY_PATH (либо apt install / docker)
- publisher запущен (cuframes-rtsp-source --key cam-parking ...)
- same IPC + PID namespace что publisher (если в docker — ipc:container: + pid:container:)
"""
import argparse
import ctypes
import sys
import time
from ctypes import c_int, c_int32, c_int64, c_uint64, c_uint32, c_char_p, c_void_p, c_size_t, POINTER, Structure
# ─── C API bindings ─────────────────────────────────────────────────────
# Error codes
CUFRAMES_OK = 0
CUFRAMES_ERR_TIMEOUT = -7
CUFRAMES_ERR_WOULD_BLOCK = -11
CUFRAMES_ERR_DISCONNECTED = -9
# Modes
CUFRAMES_MODE_NEWEST_ONLY = 0
CUFRAMES_MODE_STRICT_ORDER = 1
# Pixel format
CUFRAMES_FORMAT_NV12 = 0
class SubscriberConfig(Structure):
"""Соответствует C struct cuframes_subscriber_config."""
_fields_ = [
("key", c_char_p),
("consumer_name", c_char_p),
("mode", c_int),
("cuda_device", c_int32),
("connect_timeout_ms", c_int32),
("_reserved", c_uint64 * 4),
]
def _load_libcuframes():
"""Загрузить libcuframes.so + bind ctypes signatures."""
try:
lib = ctypes.CDLL("libcuframes.so.0")
except OSError as e:
sys.stderr.write(f"Cannot load libcuframes.so.0: {e}\n")
sys.stderr.write("Установи libcuframes (см. cuframes README) и убедись что .so в LD_LIBRARY_PATH.\n")
sys.exit(1)
# cuframes_strerror
lib.cuframes_strerror.argtypes = [c_int]
lib.cuframes_strerror.restype = c_char_p
# cuframes_subscriber_create
lib.cuframes_subscriber_create.argtypes = [POINTER(SubscriberConfig), POINTER(c_void_p)]
lib.cuframes_subscriber_create.restype = c_int
# cuframes_subscriber_next (consumer_stream=NULL — sync API, default stream)
lib.cuframes_subscriber_next.argtypes = [c_void_p, c_void_p, POINTER(c_void_p), c_int32]
lib.cuframes_subscriber_next.restype = c_int
# cuframes_subscriber_release
lib.cuframes_subscriber_release.argtypes = [c_void_p, c_void_p]
lib.cuframes_subscriber_release.restype = c_int
# cuframes_subscriber_destroy
lib.cuframes_subscriber_destroy.argtypes = [c_void_p]
lib.cuframes_subscriber_destroy.restype = c_int
# cuframes_frame_* accessors
lib.cuframes_frame_cuda_ptr.argtypes = [c_void_p]
lib.cuframes_frame_cuda_ptr.restype = c_void_p
lib.cuframes_frame_size.argtypes = [c_void_p, POINTER(c_int32), POINTER(c_int32)]
lib.cuframes_frame_size.restype = None
lib.cuframes_frame_pitch_y.argtypes = [c_void_p]
lib.cuframes_frame_pitch_y.restype = c_int32
lib.cuframes_frame_pitch_uv.argtypes = [c_void_p]
lib.cuframes_frame_pitch_uv.restype = c_int32
lib.cuframes_frame_seq.argtypes = [c_void_p]
lib.cuframes_frame_seq.restype = c_uint64
lib.cuframes_frame_pts_ns.argtypes = [c_void_p]
lib.cuframes_frame_pts_ns.restype = c_int64
return lib
# ─── Main consumer loop ────────────────────────────────────────────────
def main():
ap = argparse.ArgumentParser(description="Reference cuframes Python consumer")
ap.add_argument("--key", required=True, help="publisher key (e.g. cam-parking)")
ap.add_argument("--max-frames", type=int, default=100, help="N frames to receive (default 100)")
ap.add_argument("--cuda-device", type=int, default=0)
ap.add_argument("--timeout-ms", type=int, default=1000, help="per-frame timeout")
args = ap.parse_args()
lib = _load_libcuframes()
# Configure subscriber
cfg = SubscriberConfig()
cfg.key = args.key.encode("utf-8")
cfg.consumer_name = None # auto-generated
cfg.mode = CUFRAMES_MODE_NEWEST_ONLY
cfg.cuda_device = args.cuda_device
cfg.connect_timeout_ms = 5000
sub_handle = c_void_p()
rc = lib.cuframes_subscriber_create(ctypes.byref(cfg), ctypes.byref(sub_handle))
if rc != CUFRAMES_OK:
sys.stderr.write(f"subscribe failed: {lib.cuframes_strerror(rc).decode()}\n")
sys.exit(1)
print(f"[consumer] connected to '{args.key}'")
received = 0
first_pts = None
start_wall = None
try:
while received < args.max_frames:
frame_handle = c_void_p()
rc = lib.cuframes_subscriber_next(sub_handle, None, ctypes.byref(frame_handle),
args.timeout_ms)
if rc == CUFRAMES_ERR_TIMEOUT or rc == CUFRAMES_ERR_WOULD_BLOCK:
continue
if rc == CUFRAMES_ERR_DISCONNECTED:
print(f"[consumer] publisher disconnected — exit")
break
if rc != CUFRAMES_OK or not frame_handle.value:
sys.stderr.write(f"next failed: {lib.cuframes_strerror(rc).decode()}\n")
break
# Frame metadata
w, h = c_int32(0), c_int32(0)
lib.cuframes_frame_size(frame_handle, ctypes.byref(w), ctypes.byref(h))
pitch_y = lib.cuframes_frame_pitch_y(frame_handle)
pitch_uv = lib.cuframes_frame_pitch_uv(frame_handle)
cuda_ptr = lib.cuframes_frame_cuda_ptr(frame_handle)
seq = lib.cuframes_frame_seq(frame_handle)
pts_ns = lib.cuframes_frame_pts_ns(frame_handle)
if first_pts is None:
first_pts = pts_ns
start_wall = time.monotonic()
print(f"[consumer] first frame: {w.value}x{h.value} NV12, "
f"pitch_y={pitch_y}, pitch_uv={pitch_uv}, cuda_ptr=0x{cuda_ptr:x}")
# ─── ВАШ ML PIPELINE ЗДЕСЬ ────────────────────────────
# 1. cudaMemcpy NV12 frame → host (или используй pycuda / cupy для in-GPU pipeline)
# 2. NV12 → RGB conversion (CPU либо GPU)
# 3. inference: model(frame) → results
# 4. publish results (mqtt / API / etc)
#
# В этом skeleton — просто counter.
received += 1
if received % 25 == 0:
print(f"[consumer] received={received} seq={seq} pts_ms={pts_ns // 1_000_000}")
# CRITICAL: release frame ОБЯЗАТЕЛЬНО — иначе publisher застрянет
# (или drop new frames при ring overflow в STRICT_ORDER mode).
lib.cuframes_subscriber_release(sub_handle, frame_handle)
finally:
lib.cuframes_subscriber_destroy(sub_handle)
if received > 1 and start_wall:
elapsed = time.monotonic() - start_wall
fps = (received - 1) / elapsed if elapsed > 0 else 0
print(f"\n=== RESULT ===")
print(f"received: {received} / {args.max_frames}")
print(f"elapsed: {elapsed:.2f}s")
print(f"avg_fps: {fps:.2f}")
sys.exit(0 if received >= args.max_frames else 1)
if __name__ == "__main__":
main()
+142 -1
View File
@@ -36,7 +36,7 @@ extern "C" {
/* ─────────────────────────────────────────────────────────────────────── */
#define CUFRAMES_VERSION_MAJOR 0
#define CUFRAMES_VERSION_MINOR 1
#define CUFRAMES_VERSION_MINOR 4
#define CUFRAMES_VERSION_PATCH 0
/** @brief Runtime-версия библиотеки в формате "MAJOR.MINOR.PATCH". */
@@ -65,6 +65,11 @@ typedef enum cuframes_error {
несовпадение размеров frame'а */
CUFRAMES_ERR_WOULD_BLOCK = -11, /**< non-blocking call — no data yet */
CUFRAMES_ERR_TOO_MANY = -12, /**< превышен MAX_SUBSCRIBERS (32) */
/* v0.2 — packet ring (см. docs/protocol.md §10.15) */
CUFRAMES_ERR_PACKET_OVERSIZED = -20, /**< publish_packet size > max_packet_size */
CUFRAMES_ERR_NO_PACKET_RING = -21, /**< subscriber запросил packets, у publisher'а нет ring'а */
CUFRAMES_ERR_NO_CODEC_PARAMS = -22, /**< extradata ещё не set publisher'ом */
CUFRAMES_ERR_PACKET_OVERRUN = -23, /**< slow subscriber, packet seq уехал — resync на keyframe */
CUFRAMES_ERR_INTERNAL = -100, /**< bug в библиотеке — repro и reportить */
} cuframes_error_t;
@@ -366,6 +371,142 @@ int cuframes_async_subscriber_create(const cuframes_subscriber_config_t *cfg,
*/
int cuframes_async_subscriber_destroy(cuframes_async_subscriber_t *sub);
/* ─────────────────────────────────────────────────────────────────────── */
/* Encoded packet ring API (v0.2 — см. docs/protocol.md §10) */
/* ─────────────────────────────────────────────────────────────────────── */
/** Packet flags — биты соответствуют AV_PKT_FLAG_* у FFmpeg. */
#define CUFRAMES_PKT_FLAG_KEY 0x01u /**< IDR / keyframe */
#define CUFRAMES_PKT_FLAG_CORRUPT 0x02u /**< RTP loss / damage */
#define CUFRAMES_PKT_FLAG_DISCONTINUITY 0x04u /**< gap before this packet */
#define CUFRAMES_PKT_FLAG_LAST_IN_AU 0x08u /**< последний NAL в access unit */
typedef struct cuframes_packet_ring_options {
/** Слотов в индексе ring'а. Default 64 (≈ 2 sec @ 30fps + GOP). */
uint32_t ring_slots;
/** Размер data section ring'а в байтах. Default 8 MiB. */
uint32_t data_size;
/** Sanity guard — publisher отклонит packet > этого. Default 2 MiB. */
uint32_t max_packet_size;
/** FFmpeg AV_CODEC_ID_* (H.264 = 27, HEVC = 173). */
uint32_t codec_id;
uint64_t _reserved[4];
} cuframes_packet_ring_options_t;
/**
* @brief Активировать encoded packet ring на существующем publisher'е.
*
* Создаёт дополнительный SHM `/dev/shm/cuframes-<key>-packets`. После
* этого call'а publisher шлёт packets через `cuframes_publisher_publish_packet`.
*
* Должно быть вызвано **до** первого `publish_packet` и желательно до того
* как subscribers начнут подключаться (иначе они увидят publisher без packet
* ring и не получат packets).
*
* @param pub
* @param opts NULL = default sizing (64 slots, 8MiB data, 2MiB max). codec_id=0 = unknown.
* @return CUFRAMES_ERR_ALREADY_EXISTS если ring уже активирован
*/
int cuframes_publisher_enable_packets(cuframes_publisher_t *pub,
const cuframes_packet_ring_options_t *opts);
/**
* @brief Установить codec extradata (SPS/PPS/VPS) для packet ring.
*
* Subscribers (FFmpeg demuxer) читают extradata из shared header и подставляют
* в AVCodecContext.extradata. Должно быть вызвано до того как subscribers
* захотят decode.
*
* @param size ≤ 4096 байт (CUFRAMES_PKT_EXTRADATA_MAX)
*/
int cuframes_publisher_set_codec_extradata(cuframes_publisher_t *pub,
const void *extradata, size_t size);
/**
* @brief Опубликовать encoded packet (H.264/H.265 NAL units, Annex B).
*
* Slow consumer = overwrite oldest. Late subscriber resync'нется на last
* keyframe (см. docs/protocol.md §10.14).
*
* @param flags CUFRAMES_PKT_FLAG_* (минимум KEY на IDR — критично!)
* @return CUFRAMES_ERR_NO_PACKET_RING если не вызывали enable_packets
* @return CUFRAMES_ERR_PACKET_OVERSIZED если size > max_packet_size
*/
int cuframes_publisher_publish_packet(cuframes_publisher_t *pub,
const void *data, size_t size,
int64_t pts_ns, int64_t dts_ns,
uint32_t flags);
/* ── Subscriber-side packet API ───────────────────────────────────────── */
/** Opaque packet handle. Освобождается через release_packet. */
typedef struct cuframes_packet cuframes_packet_t;
/** @brief Pointer на encoded NAL bytes. Valid до release_packet. */
const void *cuframes_packet_data(const cuframes_packet_t *p);
/** @brief Размер payload в байтах. */
size_t cuframes_packet_size(const cuframes_packet_t *p);
/** @brief Presentation timestamp (наносекунды). */
int64_t cuframes_packet_pts(const cuframes_packet_t *p);
/** @brief Decode timestamp (для B-frames pipelines). */
int64_t cuframes_packet_dts(const cuframes_packet_t *p);
/** @brief Биты CUFRAMES_PKT_FLAG_*. */
uint32_t cuframes_packet_flags(const cuframes_packet_t *p);
/** @brief Sequence number у publisher'а. */
uint64_t cuframes_packet_seq(const cuframes_packet_t *p);
/**
* @brief Активировать чтение packet ring на subscriber'е.
*
* Открывает SHM `/dev/shm/cuframes-<key>-packets` (тот же `key` что в config).
* После этого можно читать через `cuframes_subscriber_next_packet`.
*
* Subscriber может одновременно иметь frames ring и packets ring (или один из).
*
* @return CUFRAMES_ERR_NOT_FOUND если publisher не имеет packet ring
*/
int cuframes_subscriber_enable_packets(cuframes_subscriber_t *sub);
/**
* @brief Получить следующий packet.
*
* Late subscriber (первый вызов) начинает с last_keyframe_seq publisher'а
* decoder receive'нет valid stream без glitches.
*
* Полученный packet ОБЯЗАТЕЛЬНО освободить через
* cuframes_subscriber_release_packet().
*
* @param timeout_ms <0 = блокироваться, 0 = non-blocking (WOULD_BLOCK), >0 = с таймаутом
* @return CUFRAMES_ERR_PACKET_OVERRUN — subscriber отстал, resync на keyframe (library сделает автоматически на next call)
* @return CUFRAMES_ERR_DISCONNECTED — publisher shutdown
*/
int cuframes_subscriber_next_packet(cuframes_subscriber_t *sub,
cuframes_packet_t **pkt_out,
int32_t timeout_ms);
/** @brief Освободить packet handle. NULL-safe. */
int cuframes_subscriber_release_packet(cuframes_subscriber_t *sub,
cuframes_packet_t *pkt);
/**
* @brief Получить codec parameters publisher'а.
*
* `*extradata_out` — pointer в библиотечный buffer, valid пока subscriber жив.
* Caller должен скопировать данные если хочет hold past subscriber lifetime.
*
* @return CUFRAMES_ERR_NO_CODEC_PARAMS если publisher ещё не вызвал
* set_codec_extradata
*/
int cuframes_subscriber_get_codec_params(cuframes_subscriber_t *sub,
uint32_t *codec_id_out,
const void **extradata_out,
size_t *extradata_size_out);
/* ─────────────────────────────────────────────────────────────────────── */
/* Утилиты */
/* ─────────────────────────────────────────────────────────────────────── */
+17
View File
@@ -148,6 +148,23 @@ public:
"Publisher::publish_external");
}
/* v0.2 — encoded packet ring */
void enable_packets(const cuframes_packet_ring_options_t *opts = nullptr) {
check(cuframes_publisher_enable_packets(pub_, opts),
"Publisher::enable_packets");
}
void set_codec_extradata(const void *data, size_t size) {
check(cuframes_publisher_set_codec_extradata(pub_, data, size),
"Publisher::set_codec_extradata");
}
/* Returns CUFRAMES_OK / negative error code (без throw — caller решает). */
int publish_packet(const void *data, size_t size,
int64_t pts_ns, int64_t dts_ns, uint32_t flags) noexcept {
return cuframes_publisher_publish_packet(pub_, data, size, pts_ns, dts_ns, flags);
}
cuframes_publisher_t *raw() noexcept { return pub_; }
private:
+4 -2
View File
@@ -10,6 +10,7 @@ set(CUFRAMES_SOURCES
src/producer.c
src/consumer.c
src/consumer_async.c
src/packet_ring.c
)
add_library(cuframes SHARED ${CUFRAMES_SOURCES})
@@ -18,7 +19,7 @@ add_library(cuframes_static STATIC ${CUFRAMES_SOURCES})
foreach(target cuframes cuframes_static)
target_include_directories(${target}
PUBLIC
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
@@ -33,6 +34,7 @@ foreach(target cuframes cuframes_static)
target_link_libraries(${target}
PUBLIC
CUDA::cudart
CUDA::cuda_driver # v0.4 — cuMemCreate/cuMemMap/cuMemExportToShareableHandle
Threads::Threads
rt # для shm_open
)
@@ -40,7 +42,7 @@ endforeach()
# Set SOVERSION на shared lib для ABI tracking
set_target_properties(cuframes PROPERTIES
VERSION 0.1.0
VERSION 0.4.0
SOVERSION 0
)
+296 -72
View File
@@ -1,4 +1,13 @@
/* Subscriber implementation (sync). */
/* Subscriber implementation (sync).
*
* v0.4 — VMM + POSIX FD. Принимает FDs через SCM_RIGHTS в handshake,
* импортирует через cuMemImportFromShareableHandle + cuMemMap. Не требует
* shared pid/ipc namespace с producer'ом.
*
* Sync: producer cuStreamSynchronize'ит свой stream перед atomic_store(seq).
* Consumer просто читает seq (acquire) и копирует данные через DtoD memcpy —
* никаких cudaEventWait не нужно (HW coherence на одном GPU).
*/
#include "internal.h"
#include <errno.h>
@@ -6,6 +15,7 @@
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>
#include <unistd.h>
/* Opaque frame — выдаётся subscriber'у на next() */
@@ -20,7 +30,17 @@ struct cuframes_frame {
int64_t pts_ns;
uint32_t slot_idx;
void *subscriber; /* back-ref для release() */
void *subscriber;
};
struct cuframes_packet {
uint8_t *data;
size_t capacity;
size_t size;
int64_t pts_ns;
int64_t dts_ns;
uint32_t flags;
uint64_t seq;
};
struct cuframes_subscriber {
@@ -32,18 +52,31 @@ struct cuframes_subscriber {
cuframes_shm_header_t *hdr;
char shm_name[80];
cudaEvent_t producer_event;
void *mapped_ptrs[CUFRAMES_MAX_RING];
/* v0.4 — VMM imported slots */
CUmemGenericAllocationHandle vmm_handles[CUFRAMES_MAX_RING];
CUdeviceptr vmm_ptrs[CUFRAMES_MAX_RING];
size_t vmm_slot_size;
int imported_count;
uint32_t assigned_bit;
uint64_t last_seen_seq;
/* Frame pool — переиспользуем одну frame_t structure (single-thread API).
* Опционально расширим до lock-free pool в v0.2 если нужен multi-frame. */
struct cuframes_frame frame_obj;
int frame_busy;
int has_pkt_ring;
cuframes_pkt_ring_t pkt_ring;
uint64_t last_packet_seq;
struct cuframes_packet packet_obj;
int packet_busy;
};
static const char *cu_err_str(CUresult r) {
const char *s = NULL;
cuGetErrorString(r, &s);
return s ? s : "?";
}
/* ─── Frame accessors ────────────────────────────────────────────────── */
void *cuframes_frame_cuda_ptr(const cuframes_frame_t *f) { return f ? f->cuda_ptr : NULL; }
cuframes_format_t cuframes_frame_format(const cuframes_frame_t *f) { return f ? f->format : 0; }
@@ -59,11 +92,13 @@ int64_t cuframes_frame_pts_ns(const cuframes_frame_t *f) { return f ? f->pts_ns
/* ─── Subscriber create ──────────────────────────────────────────────── */
static int do_handshake(struct cuframes_subscriber *sub, const char *name) {
/* Send HELLO_REQ */
static int do_handshake(struct cuframes_subscriber *sub, const char *name,
int *fds_out, uint32_t *fd_count_inout,
uint64_t *slot_size_out) {
/* Send HELLO_REQ — proto v4 */
uint8_t buf[CUFRAMES_MAX_MSG_PAYLOAD];
cuframes_msg_hello_req_t *hreq = (cuframes_msg_hello_req_t *)buf;
hreq->proto_version = CUFRAMES_PROTOCOL_V1;
hreq->proto_version = CUFRAMES_PROTOCOL_V4;
uint32_t nl = name ? (uint32_t)strlen(name) : 0;
if (nl > 31) nl = 31;
hreq->consumer_name_len = nl;
@@ -80,7 +115,6 @@ static int do_handshake(struct cuframes_subscriber *sub, const char *name) {
buf, plen);
if (r != CUFRAMES_OK) return r;
/* Recv HELLO_RESP */
uint32_t rmt = 0, rpl = sizeof(buf);
r = cuframes_internal_recv_msg(sub->sock_fd, &rmt, buf, &rpl, 5000);
if (r != CUFRAMES_OK) return r;
@@ -88,10 +122,15 @@ static int do_handshake(struct cuframes_subscriber *sub, const char *name) {
cuframes_msg_hello_resp_t *hresp = (cuframes_msg_hello_resp_t *)buf;
if (hresp->result != CUFRAMES_OK) return hresp->result;
if (hresp->proto_version_actual != CUFRAMES_PROTOCOL_V4) {
CUFRAMES_LOG_ERROR("publisher proto v%u — нужен v%u (v0.4)",
hresp->proto_version_actual, CUFRAMES_PROTOCOL_V4);
return CUFRAMES_ERR_PROTOCOL;
}
/* Send SUBSCRIBE_REQ */
uint32_t srbuf[8];
srbuf[0] = CUFRAMES_PROTOCOL_V1;
srbuf[0] = CUFRAMES_PROTOCOL_V4;
memset(srbuf + 1, 0, 28);
r = cuframes_internal_send_msg(sub->sock_fd, CUFRAMES_MSG_SUBSCRIBE_REQ,
srbuf, sizeof(srbuf));
@@ -106,7 +145,29 @@ static int do_handshake(struct cuframes_subscriber *sub, const char *name) {
if (sresp.result != CUFRAMES_OK) return sresp.result;
sub->assigned_bit = sresp.assigned_bit;
sub->last_seen_seq = sresp.initial_seq; /* start от текущей точки */
sub->last_seen_seq = sresp.initial_seq;
/* Recv VMM_FDS */
cuframes_msg_vmm_fds_t vmm_payload = {0};
uint32_t vmm_plen = sizeof(vmm_payload);
rmt = 0;
r = cuframes_internal_recv_msg_with_fds(sub->sock_fd, &rmt,
&vmm_payload, &vmm_plen,
fds_out, fd_count_inout, 5000);
if (r != CUFRAMES_OK) {
CUFRAMES_LOG_ERROR("recv VMM_FDS: %s", cuframes_strerror(r));
return r;
}
if (rmt != CUFRAMES_MSG_VMM_FDS) {
CUFRAMES_LOG_ERROR("expected VMM_FDS got 0x%x", rmt);
return CUFRAMES_ERR_PROTOCOL;
}
if (vmm_payload.fd_count != *fd_count_inout) {
CUFRAMES_LOG_ERROR("VMM_FDS: payload fd_count=%u, received %u",
vmm_payload.fd_count, *fd_count_inout);
return CUFRAMES_ERR_PROTOCOL;
}
*slot_size_out = vmm_payload.slot_size_bytes;
return CUFRAMES_OK;
}
@@ -123,7 +184,6 @@ int cuframes_subscriber_create(const cuframes_subscriber_config_t *cfg,
sub->sock_fd = -1;
sub->shm_fd = -1;
/* Generate fallback name if NULL */
char name_buf[32];
const char *name = cfg->consumer_name;
if (!name) {
@@ -132,12 +192,10 @@ int cuframes_subscriber_create(const cuframes_subscriber_config_t *cfg,
name = name_buf;
}
/* Build paths */
char sock_path[128];
int r = cuframes_internal_socket_path(cfg->key, sock_path, sizeof(sock_path));
if (r != CUFRAMES_OK) { free(sub); return r; }
/* Connect with timeout retry */
int64_t deadline = cfg->connect_timeout_ms > 0
? cuframes_now_ns() + (int64_t)cfg->connect_timeout_ms * 1000000LL
: 0;
@@ -152,63 +210,117 @@ int cuframes_subscriber_create(const cuframes_subscriber_config_t *cfg,
sub->sock_fd = -1;
if (cfg->connect_timeout_ms == 0) { r = CUFRAMES_ERR_NOT_FOUND; goto fail; }
if (deadline && cuframes_now_ns() > deadline) { r = CUFRAMES_ERR_TIMEOUT; goto fail; }
struct timespec ts = {.tv_sec = 0, .tv_nsec = 100000000}; /* 100ms */
struct timespec ts = {.tv_sec = 0, .tv_nsec = 100000000};
nanosleep(&ts, NULL);
}
/* Handshake */
r = do_handshake(sub, name);
/* Handshake (включая VMM_FDS) */
int fds[CUFRAMES_MAX_RING];
for (int i = 0; i < CUFRAMES_MAX_RING; i++) fds[i] = -1;
uint32_t fd_count = CUFRAMES_MAX_RING;
uint64_t slot_size = 0;
r = do_handshake(sub, name, fds, &fd_count, &slot_size);
if (r != CUFRAMES_OK) goto fail;
/* Open SHM */
/* Open SHM (для seq atomics + meta) */
r = cuframes_internal_shm_name(cfg->key, sub->shm_name, sizeof(sub->shm_name));
if (r != CUFRAMES_OK) goto fail;
if (r != CUFRAMES_OK) goto fail_close_fds;
sub->shm_fd = shm_open(sub->shm_name, O_RDWR, 0);
if (sub->shm_fd < 0) {
CUFRAMES_LOG_ERROR("shm_open %s: %s", sub->shm_name, strerror(errno));
r = CUFRAMES_ERR_IO; goto fail;
r = CUFRAMES_ERR_IO; goto fail_close_fds;
}
sub->hdr = mmap(NULL, sizeof(cuframes_shm_header_t),
PROT_READ | PROT_WRITE, MAP_SHARED, sub->shm_fd, 0);
if (sub->hdr == MAP_FAILED) {
sub->hdr = NULL;
r = CUFRAMES_ERR_IO; goto fail;
r = CUFRAMES_ERR_IO; goto fail_close_fds;
}
if (sub->hdr->magic != CUFRAMES_MAGIC) {
if (sub->hdr->magic == CUFRAMES_MAGIC_LEGACY) {
CUFRAMES_LOG_ERROR("publisher uses legacy v0.1-v0.3 SHM — нужен v0.4 publisher");
} else {
CUFRAMES_LOG_ERROR("SHM magic mismatch: 0x%x", sub->hdr->magic);
}
r = CUFRAMES_ERR_PROTOCOL; goto fail_close_fds;
}
if (sub->hdr->proto_version != CUFRAMES_PROTOCOL_V4) {
CUFRAMES_LOG_ERROR("SHM proto v%u — нужен v%u",
sub->hdr->proto_version, CUFRAMES_PROTOCOL_V4);
r = CUFRAMES_ERR_PROTOCOL; goto fail_close_fds;
}
if (sub->hdr->magic != CUFRAMES_MAGIC) { r = CUFRAMES_ERR_PROTOCOL; goto fail; }
/* CUDA setup */
/* CUDA driver init + import VMM handles */
CUresult cr = cuInit(0);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuInit: %s", cu_err_str(cr));
r = CUFRAMES_ERR_CUDA; goto fail_close_fds;
}
/* Ensure a runtime context exists (cudaMemcpyAsync from this pool needs it) */
cudaError_t cerr = cudaSetDevice(sub->cfg.cuda_device);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaSetDevice: %s", cudaGetErrorString(cerr));
r = CUFRAMES_ERR_CUDA; goto fail;
r = CUFRAMES_ERR_CUDA; goto fail_close_fds;
}
/* Open producer's event */
cerr = cudaIpcOpenEventHandle(&sub->producer_event, sub->hdr->ipc_event_handle);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaIpcOpenEventHandle: %s", cudaGetErrorString(cerr));
r = CUFRAMES_ERR_CUDA; goto fail;
}
CUmemAccessDesc access = {0};
access.location.type = CU_MEM_LOCATION_TYPE_DEVICE;
access.location.id = sub->cfg.cuda_device;
access.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
/* Open mem handles */
int ring = (int)sub->hdr->ring_size;
if (ring > CUFRAMES_MAX_RING) ring = CUFRAMES_MAX_RING;
for (int i = 0; i < ring; ++i) {
cerr = cudaIpcOpenMemHandle(&sub->mapped_ptrs[i],
sub->hdr->slots[i].mem_handle,
cudaIpcMemLazyEnablePeerAccess);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaIpcOpenMemHandle slot %d: %s",
i, cudaGetErrorString(cerr));
r = CUFRAMES_ERR_CUDA; goto fail;
sub->vmm_slot_size = (size_t)slot_size;
sub->imported_count = 0;
for (uint32_t i = 0; i < fd_count; ++i) {
cr = cuMemImportFromShareableHandle(&sub->vmm_handles[i],
(void *)(uintptr_t)fds[i],
CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuMemImportFromShareableHandle slot %u: %s",
i, cu_err_str(cr));
r = CUFRAMES_ERR_CUDA; goto fail_unmap;
}
/* После import можно закрыть FD — kernel держит reference через handle */
close(fds[i]);
fds[i] = -1;
cr = cuMemAddressReserve(&sub->vmm_ptrs[i], sub->vmm_slot_size, 0, 0, 0);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuMemAddressReserve slot %u: %s",
i, cu_err_str(cr));
r = CUFRAMES_ERR_CUDA; goto fail_unmap;
}
cr = cuMemMap(sub->vmm_ptrs[i], sub->vmm_slot_size, 0,
sub->vmm_handles[i], 0);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuMemMap slot %u: %s", i, cu_err_str(cr));
r = CUFRAMES_ERR_CUDA; goto fail_unmap;
}
cr = cuMemSetAccess(sub->vmm_ptrs[i], sub->vmm_slot_size, &access, 1);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuMemSetAccess slot %u: %s", i, cu_err_str(cr));
r = CUFRAMES_ERR_CUDA; goto fail_unmap;
}
sub->imported_count++;
}
CUFRAMES_LOG_INFO("subscriber '%s' connected to '%s' (bit=%u, ring=%d)",
name, sub->key, sub->assigned_bit, ring);
CUFRAMES_LOG_INFO("subscriber '%s' connected to '%s' (bit=%u, ring=%u, v0.4 VMM)",
name, sub->key, sub->assigned_bit, fd_count);
*out = sub;
return CUFRAMES_OK;
fail_unmap:
/* Cleanup partial VMM */
for (int i = 0; i < sub->imported_count; i++) {
if (sub->vmm_ptrs[i]) {
cuMemUnmap(sub->vmm_ptrs[i], sub->vmm_slot_size);
cuMemAddressFree(sub->vmm_ptrs[i], sub->vmm_slot_size);
}
if (sub->vmm_handles[i]) cuMemRelease(sub->vmm_handles[i]);
}
fail_close_fds:
for (int i = 0; i < CUFRAMES_MAX_RING; i++) {
if (fds[i] >= 0) close(fds[i]);
}
fail:
cuframes_subscriber_destroy(sub);
return r;
@@ -224,6 +336,7 @@ int cuframes_subscriber_next(cuframes_subscriber_t *sub,
memory_order_acquire) != 0) {
return CUFRAMES_ERR_DISCONNECTED;
}
(void)consumer_stream; /* v0.4: producer уже StreamSync'нул, sync не нужен */
int64_t deadline = (timeout_ms > 0)
? cuframes_now_ns() + (int64_t)timeout_ms * 1000000LL
@@ -237,11 +350,9 @@ int cuframes_subscriber_next(cuframes_subscriber_t *sub,
if (sub->cfg.mode == CUFRAMES_MODE_NEWEST_ONLY) {
target_seq = gs;
} else {
/* STRICT_ORDER */
if (sub->last_seen_seq == UINT64_MAX) {
target_seq = gs;
} else if (gs > sub->last_seen_seq + (uint64_t)sub->hdr->ring_size) {
/* Producer overran us. */
return CUFRAMES_ERR_DISCONNECTED;
} else {
target_seq = sub->last_seen_seq + 1;
@@ -251,30 +362,22 @@ int cuframes_subscriber_next(cuframes_subscriber_t *sub,
uint64_t slot_seq = atomic_load_explicit(&sub->hdr->slots[slot_idx].seq,
memory_order_acquire);
if (slot_seq != target_seq) {
/* Slot уже перезаписан producer'ом — пересчитать */
continue;
}
int64_t pts = atomic_load_explicit(&sub->hdr->slots[slot_idx].pts_ns,
memory_order_acquire);
/* Cross-process sync: wait event on consumer's stream */
if (consumer_stream) {
cudaError_t cerr = cudaStreamWaitEvent((cudaStream_t)consumer_stream,
sub->producer_event, 0);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_WARN("cudaStreamWaitEvent: %s",
cudaGetErrorString(cerr));
return CUFRAMES_ERR_CUDA;
}
} else {
/* Synchronize globally — для cudaMemcpyDeviceToHost users */
cudaError_t cerr = cudaEventSynchronize(sub->producer_event);
if (cerr != cudaSuccess) return CUFRAMES_ERR_CUDA;
/* v0.4: producer уже cuStreamSynchronize'нул перед atomic_store seq.
* Данные физически в GPU memory к моменту acquire fence. Post-sync
* verify оставляем — defending against ring wrap pока мы читали pts. */
uint64_t verify_seq = atomic_load_explicit(&sub->hdr->slots[slot_idx].seq,
memory_order_acquire);
if (verify_seq != target_seq) {
continue;
}
/* Fill frame_out */
struct cuframes_frame *f = &sub->frame_obj;
f->cuda_ptr = sub->mapped_ptrs[slot_idx];
f->cuda_ptr = (void *)(uintptr_t)sub->vmm_ptrs[slot_idx];
f->format = (cuframes_format_t)sub->hdr->meta.format;
f->width = sub->hdr->meta.width;
f->height = sub->hdr->meta.height;
@@ -290,12 +393,9 @@ int cuframes_subscriber_next(cuframes_subscriber_t *sub,
return CUFRAMES_OK;
}
/* Не было frame'ов */
if (timeout_ms == 0) return CUFRAMES_ERR_WOULD_BLOCK;
if (timeout_ms > 0 && cuframes_now_ns() > deadline) return CUFRAMES_ERR_TIMEOUT;
/* Poll-based wait (eventfd — v0.2). 50µs interval — компромисс
* latency vs CPU. */
struct timespec ts = {.tv_sec = 0, .tv_nsec = 50000};
nanosleep(&ts, NULL);
@@ -311,7 +411,6 @@ int cuframes_subscriber_release(cuframes_subscriber_t *sub,
if (!frame) return CUFRAMES_OK;
if (!sub || frame->subscriber != sub) return CUFRAMES_ERR_INVALID_ARG;
/* ACK через bitmap */
if (sub->assigned_bit > 0 && sub->assigned_bit < 64) {
atomic_fetch_or_explicit(&sub->hdr->slots[frame->slot_idx].ack_bitmap,
1ULL << sub->assigned_bit,
@@ -330,7 +429,6 @@ int cuframes_subscriber_release(cuframes_subscriber_t *sub,
int cuframes_subscriber_destroy(cuframes_subscriber_t *sub) {
if (!sub) return CUFRAMES_OK;
/* Clear subscriber bit */
if (sub->hdr && sub->assigned_bit > 0) {
atomic_fetch_and_explicit(&sub->hdr->subscriber_bitmap,
~(1ULL << sub->assigned_bit),
@@ -339,12 +437,21 @@ int cuframes_subscriber_destroy(cuframes_subscriber_t *sub) {
0, memory_order_release);
}
if (sub->producer_event) cudaEventDestroy(sub->producer_event);
/* VMM cleanup */
for (int i = 0; i < sub->imported_count; i++) {
if (sub->vmm_ptrs[i]) {
cuMemUnmap(sub->vmm_ptrs[i], sub->vmm_slot_size);
cuMemAddressFree(sub->vmm_ptrs[i], sub->vmm_slot_size);
}
if (sub->vmm_handles[i]) cuMemRelease(sub->vmm_handles[i]);
}
int ring = sub->hdr ? (int)sub->hdr->ring_size : 0;
if (ring > CUFRAMES_MAX_RING) ring = CUFRAMES_MAX_RING;
for (int i = 0; i < ring; ++i) {
if (sub->mapped_ptrs[i]) cudaIpcCloseMemHandle(sub->mapped_ptrs[i]);
if (sub->has_pkt_ring) {
cuframes_internal_pkt_ring_destroy(&sub->pkt_ring);
}
if (sub->packet_obj.data) {
free(sub->packet_obj.data);
sub->packet_obj.data = NULL;
}
if (sub->hdr) munmap(sub->hdr, sizeof(cuframes_shm_header_t));
@@ -353,3 +460,120 @@ int cuframes_subscriber_destroy(cuframes_subscriber_t *sub) {
free(sub);
return CUFRAMES_OK;
}
/* ─────────────────────────────────────────────────────────────────────── */
/* v0.2 — encoded packet ring API (см. docs/protocol.md §10) */
/* ─────────────────────────────────────────────────────────────────────── */
const void *cuframes_packet_data(const cuframes_packet_t *p) { return p ? p->data : NULL; }
size_t cuframes_packet_size(const cuframes_packet_t *p) { return p ? p->size : 0; }
int64_t cuframes_packet_pts(const cuframes_packet_t *p) { return p ? p->pts_ns : 0; }
int64_t cuframes_packet_dts(const cuframes_packet_t *p) { return p ? p->dts_ns : 0; }
uint32_t cuframes_packet_flags(const cuframes_packet_t *p) { return p ? p->flags : 0; }
uint64_t cuframes_packet_seq(const cuframes_packet_t *p) { return p ? p->seq : 0; }
int cuframes_subscriber_enable_packets(cuframes_subscriber_t *sub) {
if (!sub) return CUFRAMES_ERR_INVALID_ARG;
if (sub->has_pkt_ring) return CUFRAMES_OK;
char pkt_name[128];
int r = cuframes_internal_pkt_shm_name(sub->key, pkt_name, sizeof(pkt_name));
if (r != CUFRAMES_OK) return r;
r = cuframes_internal_pkt_ring_open(pkt_name, &sub->pkt_ring);
if (r != CUFRAMES_OK) return r;
size_t capacity = sub->pkt_ring.hdr->data_size;
sub->packet_obj.data = (uint8_t *)malloc(capacity);
if (!sub->packet_obj.data) {
cuframes_internal_pkt_ring_destroy(&sub->pkt_ring);
return CUFRAMES_ERR_OUT_OF_MEMORY;
}
sub->packet_obj.capacity = capacity;
uint64_t kf = atomic_load_explicit(&sub->pkt_ring.hdr->last_keyframe_seq,
memory_order_acquire);
sub->last_packet_seq = (kf == UINT64_MAX) ? UINT64_MAX : kf - 1;
sub->has_pkt_ring = 1;
return CUFRAMES_OK;
}
int cuframes_subscriber_next_packet(cuframes_subscriber_t *sub,
cuframes_packet_t **pkt_out,
int32_t timeout_ms) {
if (!sub || !pkt_out) return CUFRAMES_ERR_INVALID_ARG;
if (!sub->has_pkt_ring) return CUFRAMES_ERR_NO_PACKET_RING;
if (sub->packet_busy) return CUFRAMES_ERR_INVALID_ARG;
int64_t deadline_ns = (timeout_ms > 0) ?
cuframes_now_ns() + (int64_t)timeout_ms * 1000000LL : 0;
for (;;) {
size_t size = 0;
int64_t pts = 0, dts = 0;
uint32_t flags = 0;
uint64_t seq_attempt = sub->last_packet_seq;
int r = cuframes_internal_pkt_ring_read(&sub->pkt_ring,
&seq_attempt,
sub->packet_obj.data,
sub->packet_obj.capacity,
&size, &pts, &dts, &flags);
if (r == CUFRAMES_OK) {
sub->last_packet_seq = seq_attempt;
sub->packet_obj.size = size;
sub->packet_obj.pts_ns = pts;
sub->packet_obj.dts_ns = dts;
sub->packet_obj.flags = flags;
sub->packet_obj.seq = seq_attempt;
sub->packet_busy = 1;
*pkt_out = &sub->packet_obj;
return CUFRAMES_OK;
}
if (r == CUFRAMES_ERR_PACKET_OVERRUN) {
uint64_t kf = atomic_load_explicit(
&sub->pkt_ring.hdr->last_keyframe_seq, memory_order_acquire);
if (kf != UINT64_MAX) {
sub->last_packet_seq = kf - 1;
}
*pkt_out = NULL;
return CUFRAMES_ERR_PACKET_OVERRUN;
}
if (r != CUFRAMES_ERR_TIMEOUT) {
*pkt_out = NULL;
return r;
}
if (timeout_ms == 0) return CUFRAMES_ERR_WOULD_BLOCK;
if (timeout_ms > 0 && cuframes_now_ns() >= deadline_ns) {
return CUFRAMES_ERR_TIMEOUT;
}
struct timespec ts = {0, 1 * 1000 * 1000};
nanosleep(&ts, NULL);
}
}
int cuframes_subscriber_release_packet(cuframes_subscriber_t *sub,
cuframes_packet_t *pkt) {
if (!sub) return CUFRAMES_ERR_INVALID_ARG;
if (!pkt) return CUFRAMES_OK;
if (pkt != &sub->packet_obj) return CUFRAMES_ERR_INVALID_ARG;
sub->packet_busy = 0;
return CUFRAMES_OK;
}
int cuframes_subscriber_get_codec_params(cuframes_subscriber_t *sub,
uint32_t *codec_id_out,
const void **extradata_out,
size_t *extradata_size_out) {
if (!sub) return CUFRAMES_ERR_INVALID_ARG;
if (!sub->has_pkt_ring) return CUFRAMES_ERR_NO_PACKET_RING;
cuframes_pkt_header_t *hdr = sub->pkt_ring.hdr;
if (codec_id_out) *codec_id_out = hdr->codec_id;
if (extradata_out) *extradata_out = hdr->codec_extradata;
if (extradata_size_out) *extradata_size_out = hdr->codec_extradata_size;
if (hdr->codec_extradata_size == 0) return CUFRAMES_ERR_NO_CODEC_PARAMS;
return CUFRAMES_OK;
}
+160 -1
View File
@@ -8,6 +8,7 @@
#define CUFRAMES_INTERNAL_H
#define _GNU_SOURCE
#include <cuda.h> /* v0.4 — driver API: cuMemCreate/cuMemMap/cuMemExportToShareableHandle */
#include <cuda_runtime.h>
#include <pthread.h>
#include <stdatomic.h>
@@ -21,14 +22,33 @@
/* ─── Protocol constants ──────────────────────────────────────────────── */
#define CUFRAMES_MAGIC 0xCC7C1DCCu
#define CUFRAMES_MAGIC 0xCC7C1DCEu /* v0.4 — bumped с 0xCC7C1DCC (full ABI break) */
#define CUFRAMES_MAGIC_LEGACY 0xCC7C1DCCu /* v0.1—v0.3 magic; ловится consumer'ом как clean PROTOCOL error */
#define CUFRAMES_PROTOCOL_V1 1u
#define CUFRAMES_PROTOCOL_V2 2u /* v0.2 — packet ring support */
#define CUFRAMES_PROTOCOL_V3 3u /* v0.3 — per-slot CUDA events (deprecated; не работает без pid share) */
#define CUFRAMES_PROTOCOL_V4 4u /* v0.4 — VMM + POSIX FD: pid/ipc namespace share не требуется */
#define CUFRAMES_MAX_SUBSCRIBERS 32
#define CUFRAMES_MAX_RING 16
#define CUFRAMES_MAX_KEY_LEN 63
#define CUFRAMES_MAX_NAME_LEN 31
#define CUFRAMES_RUNTIME_DIR "/run/cuframes"
#define CUFRAMES_SHM_PREFIX "/cuframes-"
#define CUFRAMES_PKT_SHM_SUFFIX "-packets" /* /cuframes-<key>-packets */
/* Packet ring constants (см. docs/protocol.md §10) */
#define CUFRAMES_PKT_MAGIC 0xCC7C1DCDu /* frames magic + 1 */
#define CUFRAMES_PKT_EXTRADATA_MAX 4096u
#define CUFRAMES_PKT_DEFAULT_SLOTS 64u
#define CUFRAMES_PKT_DEFAULT_DATA_SIZE (8u * 1024u * 1024u) /* 8 MB */
#define CUFRAMES_PKT_DEFAULT_MAX_SIZE (2u * 1024u * 1024u) /* 2 MB */
#define CUFRAMES_PKT_MAX_SLOTS 1024u
/* Packet flags (см. docs/protocol.md §10.6) */
#define CUFRAMES_PKT_FLAG_KEY 0x01u
#define CUFRAMES_PKT_FLAG_CORRUPT 0x02u
#define CUFRAMES_PKT_FLAG_DISCONTINUITY 0x04u
#define CUFRAMES_PKT_FLAG_LAST_IN_AU 0x08u
/* ─── Shared memory layout (см. docs/protocol.md §2) ──────────────────── */
@@ -91,6 +111,11 @@ typedef struct __attribute__((packed)) cuframes_shm_header {
/* offset 0x100 — variable-length tail */
cuframes_shm_slot_t slots[CUFRAMES_MAX_RING]; /* 192 × 16 = 3072 */
cuframes_shm_subscriber_t subscribers[CUFRAMES_MAX_SUBSCRIBERS]; /* 128 × 32 = 4096 */
/* v0.3 — per-slot CUDA event handles. Producer records event per publish;
* consumer waits event[slot_idx] specifically (не global ipc_event_handle
* который signals только для последнего published frame). Закрывает TOCTOU
* race в slot read. 64 × 16 = 1024 bytes. */
cudaIpcEventHandle_t slot_event_handles[CUFRAMES_MAX_RING];
} cuframes_shm_header_t;
/* Layout sanity checks (docs/protocol.md §2 table) */
@@ -103,6 +128,73 @@ _Static_assert(offsetof(cuframes_shm_header_t, ipc_event_handle) == 0x0080, "eve
_Static_assert(offsetof(cuframes_shm_header_t, global_seq) == 0x00C0, "global_seq offset");
_Static_assert(offsetof(cuframes_shm_header_t, slots) == 0x0100, "slots offset");
/* ─── Packet ring shared memory layout (docs/protocol.md §10) ──────────── */
/* Packet slot entry — packed 64 байт */
typedef struct __attribute__((packed)) cuframes_pkt_slot {
_Atomic uint64_t seq; /* UINT64_MAX = invalid */
int64_t pts_ns;
int64_t dts_ns;
uint64_t data_offset; /* absolute byte cursor; % data_size = ring offset */
uint32_t data_size;
uint32_t flags;
uint8_t reserved[24];
} cuframes_pkt_slot_t;
_Static_assert(sizeof(cuframes_pkt_slot_t) == 64, "packet slot must be 64 bytes");
/* Packet ring header (fixed 0x1040 = 4160 bytes). Followed by slots[N] + data[]. */
typedef struct __attribute__((packed)) cuframes_pkt_header {
uint32_t magic; /* CUFRAMES_PKT_MAGIC */
uint32_t proto_version; /* 2 */
uint32_t ring_slots;
uint32_t data_size;
uint32_t codec_id; /* AV_CODEC_ID_H264 / HEVC / ... */
uint32_t codec_extradata_size; /* ≤ CUFRAMES_PKT_EXTRADATA_MAX */
uint64_t producer_pid;
_Atomic uint64_t global_seq;
_Atomic uint64_t last_keyframe_seq;
_Atomic uint64_t write_offset;
_Atomic uint64_t shutdown_flag;
uint8_t codec_extradata[CUFRAMES_PKT_EXTRADATA_MAX];
/* offset 0x1040 — slots[ring_slots], then data[data_size] */
} cuframes_pkt_header_t;
_Static_assert(offsetof(cuframes_pkt_header_t, magic) == 0x0000, "pkt magic offset");
_Static_assert(offsetof(cuframes_pkt_header_t, proto_version) == 0x0004, "pkt proto offset");
_Static_assert(offsetof(cuframes_pkt_header_t, producer_pid) == 0x0018, "pkt pid offset");
_Static_assert(offsetof(cuframes_pkt_header_t, global_seq) == 0x0020, "pkt global_seq offset");
_Static_assert(offsetof(cuframes_pkt_header_t, write_offset) == 0x0030, "pkt write_offset offset");
_Static_assert(offsetof(cuframes_pkt_header_t, codec_extradata) == 0x0040, "pkt extradata offset");
_Static_assert(sizeof(cuframes_pkt_header_t) == 0x1040, "pkt header must be 0x1040 bytes");
/* Computed SHM layout helper:
* total = sizeof(cuframes_pkt_header_t) + slots*sizeof(slot) + data_size
*/
static inline size_t cuframes_pkt_shm_size(uint32_t slots, uint32_t data_size) {
return sizeof(cuframes_pkt_header_t)
+ (size_t)slots * sizeof(cuframes_pkt_slot_t)
+ (size_t)data_size;
}
/* Pointers into mmap'ed pkt SHM (computed from header base) */
static inline cuframes_pkt_slot_t * cuframes_pkt_slots(cuframes_pkt_header_t *hdr) {
return (cuframes_pkt_slot_t *)((uint8_t *)hdr + sizeof(cuframes_pkt_header_t));
}
static inline uint8_t * cuframes_pkt_data(cuframes_pkt_header_t *hdr) {
return (uint8_t *)hdr + sizeof(cuframes_pkt_header_t)
+ (size_t)hdr->ring_slots * sizeof(cuframes_pkt_slot_t);
}
/* Opaque ring handle — содержит state и mapping для publisher или subscriber. */
typedef struct cuframes_pkt_ring {
int shm_fd;
void *shm_base;
size_t shm_size;
cuframes_pkt_header_t *hdr;
char shm_name[128]; /* /cuframes-<key>-packets */
int is_publisher;
} cuframes_pkt_ring_t;
/* ─── Socket protocol messages (docs/protocol.md §3) ───────────────────── */
#define CUFRAMES_MSG_HELLO_REQ 0x01
@@ -115,6 +207,10 @@ _Static_assert(offsetof(cuframes_shm_header_t, slots) == 0x0100, "slots offset")
#define CUFRAMES_MSG_PING 0xF0
#define CUFRAMES_MSG_PONG 0xF1
#define CUFRAMES_MSG_ERROR 0xFE
/* v0.4: после SUBSCRIBE_RESP publisher шлёт VMM_FDS с N posix FD handles в
* SCM_RIGHTS control. Payload: uint64_t slot_size + uint32_t fd_count +
* uint32_t reserved (для alignment). FDs приходят отдельным контрол-блоком. */
#define CUFRAMES_MSG_VMM_FDS 0x05
#define CUFRAMES_MAX_MSG_PAYLOAD 4096
@@ -148,6 +244,14 @@ typedef struct __attribute__((packed)) cuframes_msg_subscribe_resp {
uint8_t reserved[12];
} cuframes_msg_subscribe_resp_t;
/* v0.4: payload VMM_FDS message. Сами FDs идут в SCM_RIGHTS control-msg
* (см. cuframes_internal_send_msg_with_fds). */
typedef struct __attribute__((packed)) cuframes_msg_vmm_fds {
uint64_t slot_size_bytes; /* физический размер одного slot после round-up к granularity */
uint32_t fd_count; /* должно совпадать с ring_size */
uint32_t reserved;
} cuframes_msg_vmm_fds_t;
/* ─── Logging (minimal — to stderr) ────────────────────────────────────── */
#define CUFRAMES_LOG_ERROR(fmt, ...) \
@@ -164,6 +268,8 @@ typedef struct __attribute__((packed)) cuframes_msg_subscribe_resp {
int cuframes_internal_socket_path(const char *key, char *out, size_t out_size);
/* Build /cuframes-<key> (for shm_open) */
int cuframes_internal_shm_name(const char *key, char *out, size_t out_size);
/* Build /cuframes-<key>-packets (for shm_open) */
int cuframes_internal_pkt_shm_name(const char *key, char *out, size_t out_size);
/* Validate key per protocol.md (alphanum/_/-, 1..63 chars) */
int cuframes_internal_validate_key(const char *key);
/* Calculate frame size + pitch для format/W/H */
@@ -181,4 +287,57 @@ int cuframes_internal_recv_msg(int sock_fd, uint32_t *msg_type_out,
void *payload, uint32_t *payload_len_inout,
int32_t timeout_ms);
/* v0.4 — send/recv с FD-attached. Используется только для VMM_FDS message. */
int cuframes_internal_send_msg_with_fds(int sock_fd, uint32_t msg_type,
const void *payload, uint32_t payload_len,
const int *fds, uint32_t fd_count);
int cuframes_internal_recv_msg_with_fds(int sock_fd, uint32_t *msg_type_out,
void *payload, uint32_t *payload_len_inout,
int *fds_out, uint32_t *fd_count_inout,
int32_t timeout_ms);
/* ─── Packet ring helpers (libcuframes/src/packet_ring.c) ─────────────── */
/* Publisher: create SHM + initialize header + slots. Stale recovery как у frames. */
int cuframes_internal_pkt_ring_create(const char *key,
uint32_t slots,
uint32_t data_size,
uint32_t codec_id,
cuframes_pkt_ring_t *ring_out);
/* Publisher: set codec extradata (SPS/PPS). Must be called before first publish.
* Если size > CUFRAMES_PKT_EXTRADATA_MAX → ERR_INVALID_ARG. */
int cuframes_internal_pkt_ring_set_extradata(cuframes_pkt_ring_t *ring,
const void *extradata,
size_t size);
/* Publisher: publish single encoded packet. Slow consumer = overwrite oldest.
* Returns CUFRAMES_ERR_PACKET_OVERSIZED если size > data_size. */
int cuframes_internal_pkt_ring_publish(cuframes_pkt_ring_t *ring,
const void *data, size_t size,
int64_t pts_ns, int64_t dts_ns,
uint32_t flags);
/* Subscriber: open existing SHM by shm name (from HELLO_RESP packet_shm_path). */
int cuframes_internal_pkt_ring_open(const char *shm_name,
cuframes_pkt_ring_t *ring_out);
/* Subscriber: read next packet.
* *seq_inout — currently held seq (we read seq_inout+1); updated on success.
* out_buf must have ≥ max_packet_size bytes; out_size receives actual size.
* Returns:
* CUFRAMES_OK on success
* CUFRAMES_ERR_PACKET_OVERRUN если publisher уехал — caller resync on keyframe
* CUFRAMES_ERR_TIMEOUT если нет нового packet
* CUFRAMES_ERR_DISCONNECTED если publisher shutdown */
int cuframes_internal_pkt_ring_read(cuframes_pkt_ring_t *ring,
uint64_t *seq_inout,
void *out_buf, size_t out_buf_max,
size_t *out_size,
int64_t *out_pts, int64_t *out_dts,
uint32_t *out_flags);
/* Publisher OR Subscriber: cleanup mmap + close FD. Publisher additionally shm_unlink. */
void cuframes_internal_pkt_ring_destroy(cuframes_pkt_ring_t *ring);
#endif /* CUFRAMES_INTERNAL_H */
+380
View File
@@ -0,0 +1,380 @@
/* libcuframes/src/packet_ring.c
*
* Variable-length encoded packet ring buffer (docs/protocol.md §10).
*
* Использует POSIX shared memory (`/cuframes-<key>-packets`), packed
* structures с _Atomic полями, seqlock-style read для защиты от overrun
* mid-read.
*
* Этот модуль внутренний — exposed API будет в Step 3 (cuframes.h
* extension). Сейчас functions имеют prefix `cuframes_internal_pkt_ring_*`
* и используются из producer.c / consumer.c.
*/
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "internal.h"
/* ─── Internal helpers ────────────────────────────────────────────────── */
static void wraparound_memcpy(uint8_t *dst, const uint8_t *src, size_t n,
size_t buf_size, size_t offset) {
/* Запись n байт начиная с offset в buf размера buf_size, wraparound. */
size_t off = offset % buf_size;
size_t first = n;
if (first > buf_size - off) first = buf_size - off;
memcpy(dst + off, src, first);
if (first < n) {
memcpy(dst, src + first, n - first);
}
}
static void wraparound_memcpy_from(uint8_t *out, const uint8_t *buf,
size_t buf_size, size_t offset, size_t n) {
/* Чтение n байт из buf с wraparound от offset. */
size_t off = offset % buf_size;
size_t first = n;
if (first > buf_size - off) first = buf_size - off;
memcpy(out, buf + off, first);
if (first < n) {
memcpy(out + first, buf, n - first);
}
}
/* ─── Publisher API ───────────────────────────────────────────────────── */
int cuframes_internal_pkt_ring_create(const char *key,
uint32_t slots,
uint32_t data_size,
uint32_t codec_id,
cuframes_pkt_ring_t *ring_out) {
if (!ring_out) return CUFRAMES_ERR_INVALID_ARG;
if (slots == 0 || slots > CUFRAMES_PKT_MAX_SLOTS) return CUFRAMES_ERR_INVALID_ARG;
if (data_size == 0) return CUFRAMES_ERR_INVALID_ARG;
memset(ring_out, 0, sizeof(*ring_out));
ring_out->shm_fd = -1;
ring_out->is_publisher = 1;
int r = cuframes_internal_pkt_shm_name(key, ring_out->shm_name,
sizeof(ring_out->shm_name));
if (r != CUFRAMES_OK) return r;
/* Stale recovery (как в frames SHM) */
int fd = shm_open(ring_out->shm_name, O_CREAT | O_EXCL | O_RDWR, 0644);
if (fd < 0) {
if (errno == EEXIST) {
int existing = shm_open(ring_out->shm_name, O_RDWR, 0);
if (existing >= 0) {
cuframes_pkt_header_t tmp;
ssize_t rb = read(existing, &tmp, sizeof(tmp));
close(existing);
if (rb == (ssize_t)sizeof(tmp) && tmp.magic == CUFRAMES_PKT_MAGIC) {
if (cuframes_internal_pid_alive((pid_t)tmp.producer_pid)) {
CUFRAMES_LOG_ERROR("packet ring %s: publisher pid %lu still alive",
ring_out->shm_name,
(unsigned long)tmp.producer_pid);
return CUFRAMES_ERR_ALREADY_EXISTS;
}
}
}
CUFRAMES_LOG_INFO("stale packet shm %s — unlinking", ring_out->shm_name);
shm_unlink(ring_out->shm_name);
fd = shm_open(ring_out->shm_name, O_CREAT | O_EXCL | O_RDWR, 0644);
if (fd < 0) {
CUFRAMES_LOG_ERROR("packet shm_open after unlink: %s", strerror(errno));
return CUFRAMES_ERR_IO;
}
} else {
CUFRAMES_LOG_ERROR("packet shm_open: %s", strerror(errno));
return CUFRAMES_ERR_IO;
}
}
size_t total_size = cuframes_pkt_shm_size(slots, data_size);
if (ftruncate(fd, (off_t)total_size) < 0) {
CUFRAMES_LOG_ERROR("packet ftruncate(%zu): %s", total_size, strerror(errno));
close(fd);
shm_unlink(ring_out->shm_name);
return CUFRAMES_ERR_IO;
}
void *base = mmap(NULL, total_size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
if (base == MAP_FAILED) {
CUFRAMES_LOG_ERROR("packet mmap: %s", strerror(errno));
close(fd);
shm_unlink(ring_out->shm_name);
return CUFRAMES_ERR_IO;
}
ring_out->shm_fd = fd;
ring_out->shm_base = base;
ring_out->shm_size = total_size;
ring_out->hdr = (cuframes_pkt_header_t *)base;
/* Initialize header — нули + magic/version/sizes */
memset(ring_out->hdr, 0, sizeof(*ring_out->hdr));
ring_out->hdr->magic = CUFRAMES_PKT_MAGIC;
ring_out->hdr->proto_version = CUFRAMES_PROTOCOL_V2;
ring_out->hdr->ring_slots = slots;
ring_out->hdr->data_size = data_size;
ring_out->hdr->codec_id = codec_id;
ring_out->hdr->codec_extradata_size = 0;
ring_out->hdr->producer_pid = (uint64_t)getpid();
atomic_store_explicit(&ring_out->hdr->global_seq, UINT64_MAX,
memory_order_release);
atomic_store_explicit(&ring_out->hdr->last_keyframe_seq, UINT64_MAX,
memory_order_release);
atomic_store_explicit(&ring_out->hdr->write_offset, 0,
memory_order_release);
atomic_store_explicit(&ring_out->hdr->shutdown_flag, 0,
memory_order_release);
/* Initialize slots — invalid seq markers */
cuframes_pkt_slot_t *slots_arr = cuframes_pkt_slots(ring_out->hdr);
for (uint32_t i = 0; i < slots; ++i) {
atomic_store_explicit(&slots_arr[i].seq, UINT64_MAX,
memory_order_release);
}
/* Data section уже zeroed через ftruncate (POSIX guarantees) */
CUFRAMES_LOG_INFO("packet ring %s: slots=%u data_size=%u codec_id=%u (total=%zu bytes)",
ring_out->shm_name, slots, data_size, codec_id, total_size);
return CUFRAMES_OK;
}
int cuframes_internal_pkt_ring_set_extradata(cuframes_pkt_ring_t *ring,
const void *extradata,
size_t size) {
if (!ring || !ring->hdr) return CUFRAMES_ERR_INVALID_ARG;
if (!ring->is_publisher) return CUFRAMES_ERR_INVALID_ARG;
if (size > CUFRAMES_PKT_EXTRADATA_MAX) return CUFRAMES_ERR_INVALID_ARG;
if (size > 0 && !extradata) return CUFRAMES_ERR_INVALID_ARG;
/* Записываем сначала bytes, потом size (release-style — subscriber видит size>0 только когда extradata готов). */
if (size > 0) {
memcpy(ring->hdr->codec_extradata, extradata, size);
/* Memory barrier — extradata stores complete до size update. */
__atomic_thread_fence(__ATOMIC_RELEASE);
}
ring->hdr->codec_extradata_size = (uint32_t)size;
return CUFRAMES_OK;
}
int cuframes_internal_pkt_ring_publish(cuframes_pkt_ring_t *ring,
const void *data, size_t size,
int64_t pts_ns, int64_t dts_ns,
uint32_t flags) {
if (!ring || !ring->hdr) return CUFRAMES_ERR_INVALID_ARG;
if (!ring->is_publisher) return CUFRAMES_ERR_INVALID_ARG;
if (size == 0 || !data) return CUFRAMES_ERR_INVALID_ARG;
if (size > ring->hdr->data_size) return CUFRAMES_ERR_PACKET_OVERSIZED;
cuframes_pkt_header_t *hdr = ring->hdr;
/* Allocate next seq + cursor offset. Single-publisher — без CAS. */
uint64_t prev_seq = atomic_load_explicit(&hdr->global_seq,
memory_order_relaxed);
uint64_t new_seq = (prev_seq == UINT64_MAX) ? 0 : prev_seq + 1;
uint64_t write_off = atomic_load_explicit(&hdr->write_offset,
memory_order_relaxed);
/* Записать payload в data ring (wraparound aware) */
wraparound_memcpy(cuframes_pkt_data(hdr), data, size,
hdr->data_size, write_off);
/* Записать slot metadata. Slot index = seq % ring_slots. */
uint32_t slot_idx = (uint32_t)(new_seq % hdr->ring_slots);
cuframes_pkt_slot_t *slot = &cuframes_pkt_slots(hdr)[slot_idx];
slot->pts_ns = pts_ns;
slot->dts_ns = dts_ns;
slot->data_offset = write_off;
slot->data_size = (uint32_t)size;
slot->flags = flags;
/* RELEASE order — payload bytes + slot metadata готовы перед publish seq. */
atomic_store_explicit(&slot->seq, new_seq, memory_order_release);
/* Update global cursor + global_seq. */
atomic_store_explicit(&hdr->write_offset, write_off + size,
memory_order_release);
atomic_store_explicit(&hdr->global_seq, new_seq,
memory_order_release);
/* Keyframe — update last_keyframe_seq для late subscribers. */
if (flags & CUFRAMES_PKT_FLAG_KEY) {
atomic_store_explicit(&hdr->last_keyframe_seq, new_seq,
memory_order_release);
}
return CUFRAMES_OK;
}
/* ─── Subscriber API ──────────────────────────────────────────────────── */
int cuframes_internal_pkt_ring_open(const char *shm_name,
cuframes_pkt_ring_t *ring_out) {
if (!shm_name || !ring_out) return CUFRAMES_ERR_INVALID_ARG;
memset(ring_out, 0, sizeof(*ring_out));
ring_out->shm_fd = -1;
ring_out->is_publisher = 0;
strncpy(ring_out->shm_name, shm_name, sizeof(ring_out->shm_name) - 1);
int fd = shm_open(shm_name, O_RDONLY, 0);
if (fd < 0) {
if (errno == ENOENT) return CUFRAMES_ERR_NOT_FOUND;
CUFRAMES_LOG_ERROR("packet shm_open(%s) ro: %s", shm_name, strerror(errno));
return CUFRAMES_ERR_IO;
}
/* Прочитать header чтобы узнать total size */
cuframes_pkt_header_t header_peek;
ssize_t rb = read(fd, &header_peek, sizeof(header_peek));
if (rb != (ssize_t)sizeof(header_peek)) {
close(fd);
return CUFRAMES_ERR_IO;
}
if (header_peek.magic != CUFRAMES_PKT_MAGIC) {
CUFRAMES_LOG_ERROR("packet shm %s: bad magic 0x%08x", shm_name, header_peek.magic);
close(fd);
return CUFRAMES_ERR_PROTOCOL;
}
if (header_peek.proto_version != CUFRAMES_PROTOCOL_V2) {
CUFRAMES_LOG_ERROR("packet shm %s: proto_version=%u (expected %u)",
shm_name, header_peek.proto_version, CUFRAMES_PROTOCOL_V2);
close(fd);
return CUFRAMES_ERR_PROTOCOL;
}
size_t total = cuframes_pkt_shm_size(header_peek.ring_slots,
header_peek.data_size);
/* mmap полностью read-only */
void *base = mmap(NULL, total, PROT_READ, MAP_SHARED, fd, 0);
if (base == MAP_FAILED) {
CUFRAMES_LOG_ERROR("packet mmap ro: %s", strerror(errno));
close(fd);
return CUFRAMES_ERR_IO;
}
ring_out->shm_fd = fd;
ring_out->shm_base = base;
ring_out->shm_size = total;
ring_out->hdr = (cuframes_pkt_header_t *)base;
CUFRAMES_LOG_INFO("packet ring %s opened: slots=%u data_size=%u",
shm_name, header_peek.ring_slots, header_peek.data_size);
return CUFRAMES_OK;
}
int cuframes_internal_pkt_ring_read(cuframes_pkt_ring_t *ring,
uint64_t *seq_inout,
void *out_buf, size_t out_buf_max,
size_t *out_size,
int64_t *out_pts, int64_t *out_dts,
uint32_t *out_flags) {
if (!ring || !ring->hdr || !seq_inout || !out_buf || !out_size
|| !out_pts || !out_dts || !out_flags) {
return CUFRAMES_ERR_INVALID_ARG;
}
cuframes_pkt_header_t *hdr = ring->hdr;
/* Publisher shutdown? */
if (atomic_load_explicit(&hdr->shutdown_flag, memory_order_acquire) != 0) {
return CUFRAMES_ERR_DISCONNECTED;
}
/* Текущий published seq */
uint64_t cur = atomic_load_explicit(&hdr->global_seq, memory_order_acquire);
if (cur == UINT64_MAX) return CUFRAMES_ERR_TIMEOUT; /* нет published */
if (*seq_inout != UINT64_MAX && cur <= *seq_inout) {
return CUFRAMES_ERR_TIMEOUT;
}
/* Calculate the next seq we want (handle первый read с UINT64_MAX → start с 0) */
uint64_t want_seq = (*seq_inout == UINT64_MAX) ? 0 : (*seq_inout + 1);
/* Если want_seq < cur и slot уже перезаписан — попадаем в OVERRUN */
if (cur - want_seq >= hdr->ring_slots) {
/* Скорее всего slot уже rewritten. Подсказка caller'у — resync. */
return CUFRAMES_ERR_PACKET_OVERRUN;
}
uint32_t slot_idx = (uint32_t)(want_seq % hdr->ring_slots);
cuframes_pkt_slot_t *slot = &cuframes_pkt_slots(hdr)[slot_idx];
/* Seqlock-style read: load seq, prove not overwritten после copy. */
uint64_t s1 = atomic_load_explicit(&slot->seq, memory_order_acquire);
if (s1 != want_seq) {
/* Slot уже занят следующим packet'ом — overrun. */
return CUFRAMES_ERR_PACKET_OVERRUN;
}
/* Снять metadata (non-atomic — read OK поскольку post-check защищает) */
uint64_t data_off = slot->data_offset;
uint32_t data_sz = slot->data_size;
int64_t pts = slot->pts_ns;
int64_t dts = slot->dts_ns;
uint32_t flags = slot->flags;
if (data_sz > out_buf_max) {
return CUFRAMES_ERR_INVALID_ARG; /* caller's buf too small */
}
/* Copy payload */
wraparound_memcpy_from((uint8_t *)out_buf,
cuframes_pkt_data(hdr),
hdr->data_size, data_off, data_sz);
/* Post-check: slot->seq не изменился во время copy. */
uint64_t s2 = atomic_load_explicit(&slot->seq, memory_order_acquire);
if (s2 != want_seq) {
return CUFRAMES_ERR_PACKET_OVERRUN;
}
*out_size = data_sz;
*out_pts = pts;
*out_dts = dts;
*out_flags = flags;
*seq_inout = want_seq;
return CUFRAMES_OK;
}
/* ─── Cleanup ─────────────────────────────────────────────────────────── */
void cuframes_internal_pkt_ring_destroy(cuframes_pkt_ring_t *ring) {
if (!ring) return;
if (ring->is_publisher && ring->hdr) {
/* Сигнализируем consumer'ам shutdown */
atomic_store_explicit(&ring->hdr->shutdown_flag, 1,
memory_order_release);
}
if (ring->shm_base && ring->shm_size > 0) {
munmap(ring->shm_base, ring->shm_size);
}
if (ring->shm_fd >= 0) {
close(ring->shm_fd);
}
if (ring->is_publisher && ring->shm_name[0] != '\0') {
shm_unlink(ring->shm_name);
}
memset(ring, 0, sizeof(*ring));
ring->shm_fd = -1;
}
+269 -165
View File
@@ -1,4 +1,14 @@
/* Publisher implementation (docs/protocol.md §1, §2, §3.2, §4.2, §5). */
/* Publisher implementation (docs/protocol.md §1, §2, §3.2, §4.2, §5).
*
* v0.4 — VMM + POSIX FD. Заменяет cudaMalloc+cudaIpcGetMemHandle на
* cuMemCreate + cuMemExportToShareableHandle(POSIX_FILE_DESCRIPTOR). FDs
* передаются consumer'у через SCM_RIGHTS, не нужны shared pid/ipc namespace.
*
* Sync (вместо cudaEventRecord+cudaIpcEventHandle): cuStreamSynchronize в
* do_publish — producer ждёт ~ms что stream flush'нулся, потом publishes seq.
* Consumer читает данные через DtoD копию без event wait — HW coherence
* гарантирована на одном GPU.
*/
#include "internal.h"
#include <errno.h>
@@ -20,10 +30,18 @@ struct cuframes_publisher {
char socket_path[128];
char shm_name[80];
/* CUDA */
cudaEvent_t event;
cudaIpcMemHandle_t ipc_mem[CUFRAMES_MAX_RING];
void *cuda_ptrs[CUFRAMES_MAX_RING]; /* mapped pointers */
/* v0.4 — VMM-allocated pool. Каждый slot: cuMemCreate → cuMemAddressReserve
* → cuMemMap → cuMemSetAccess. FD экспортируется один раз и передаётся всем
* subscribers через SCM_RIGHTS. */
CUmemGenericAllocationHandle vmm_handles[CUFRAMES_MAX_RING];
CUdeviceptr vmm_ptrs[CUFRAMES_MAX_RING];
int vmm_fds[CUFRAMES_MAX_RING];
size_t vmm_slot_size; /* rounded к granularity */
int has_vmm_pool;
/* CUDA stream sync — заменяет per-slot events. Producer перед каждым publish
* вызывает cuStreamSynchronize чтобы гарантировать что previous writes
* завершены (data visible для consumer'ов на любом GPU stream). */
size_t frame_size_bytes;
int32_t ring_size_actual;
@@ -32,22 +50,31 @@ struct cuframes_publisher {
int32_t current_slot; /* индекс slot'а полученного через acquire() */
int has_acquired;
/* EXTERNAL ownership: map user pointer → ring index */
void *external_ptrs[CUFRAMES_MAX_RING];
int32_t external_count;
/* Subscriber-management thread */
pthread_t accept_thread;
int accept_thread_alive;
int stop_flag;
pthread_mutex_t state_mu; /* protects subscriber connections */
/* v0.2 — encoded packet ring (optional). is_pkt_ring=1 → активирован. */
int has_pkt_ring;
uint32_t max_packet_size;
cuframes_pkt_ring_t pkt_ring;
};
/* Forward decls */
static void *accept_thread_main(void *arg);
static int handshake_subscriber(struct cuframes_publisher *pub, int client_fd);
static void free_vmm_pool(struct cuframes_publisher *pub);
/* ─── Internal: alloc/setup CUDA pool and SHM ─────────────────────────── */
/* Helper: format CUresult error для CUFRAMES_LOG_ERROR */
static const char *cu_err_str(CUresult r) {
const char *s = NULL;
cuGetErrorString(r, &s);
return s ? s : "?";
}
/* ─── Internal: alloc VMM pool + export POSIX FDs ─────────────────────── */
static int alloc_library_pool(struct cuframes_publisher *pub) {
int r = cuframes_internal_calc_size(pub->cfg.format,
@@ -56,7 +83,37 @@ static int alloc_library_pool(struct cuframes_publisher *pub) {
if (r != CUFRAMES_OK) return r;
pub->ring_size_actual = pub->cfg.ring_size;
for (int i = 0; i < CUFRAMES_MAX_RING; i++) pub->vmm_fds[i] = -1;
/* Initialize CUDA driver API context */
CUresult cr = cuInit(0);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuInit: %s", cu_err_str(cr));
return CUFRAMES_ERR_CUDA;
}
/* Pick allocation prop: pinned device memory с POSIX FD handle */
CUmemAllocationProp prop = {0};
prop.type = CU_MEM_ALLOCATION_TYPE_PINNED;
prop.location.type = CU_MEM_LOCATION_TYPE_DEVICE;
prop.location.id = pub->cfg.cuda_device;
prop.requestedHandleTypes = CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR;
/* Round slot size up to granularity */
size_t granularity = 0;
cr = cuMemGetAllocationGranularity(&granularity, &prop,
CU_MEM_ALLOC_GRANULARITY_MINIMUM);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuMemGetAllocationGranularity: %s", cu_err_str(cr));
return CUFRAMES_ERR_CUDA;
}
pub->vmm_slot_size = ((pub->frame_size_bytes + granularity - 1) / granularity)
* granularity;
CUFRAMES_LOG_INFO("VMM granularity=%zu frame=%zu slot=%zu",
granularity, pub->frame_size_bytes, pub->vmm_slot_size);
/* Required: also need a runtime API context so that cudaMemcpyAsync from
* user works on this allocation. cudaSetDevice достаточно. */
cudaError_t cerr = cudaSetDevice(pub->cfg.cuda_device);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaSetDevice(%d): %s",
@@ -64,59 +121,68 @@ static int alloc_library_pool(struct cuframes_publisher *pub) {
return CUFRAMES_ERR_CUDA;
}
CUmemAccessDesc access = {0};
access.location = prop.location;
access.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
for (int i = 0; i < pub->ring_size_actual; ++i) {
cerr = cudaMalloc(&pub->cuda_ptrs[i], pub->frame_size_bytes);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaMalloc slot %d: %s",
i, cudaGetErrorString(cerr));
cr = cuMemCreate(&pub->vmm_handles[i], pub->vmm_slot_size, &prop, 0);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuMemCreate slot %d: %s", i, cu_err_str(cr));
free_vmm_pool(pub);
return CUFRAMES_ERR_CUDA;
}
cerr = cudaIpcGetMemHandle(&pub->ipc_mem[i], pub->cuda_ptrs[i]);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaIpcGetMemHandle slot %d: %s",
i, cudaGetErrorString(cerr));
cr = cuMemAddressReserve(&pub->vmm_ptrs[i], pub->vmm_slot_size, 0, 0, 0);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuMemAddressReserve slot %d: %s", i, cu_err_str(cr));
free_vmm_pool(pub);
return CUFRAMES_ERR_CUDA;
}
cr = cuMemMap(pub->vmm_ptrs[i], pub->vmm_slot_size, 0,
pub->vmm_handles[i], 0);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuMemMap slot %d: %s", i, cu_err_str(cr));
free_vmm_pool(pub);
return CUFRAMES_ERR_CUDA;
}
cr = cuMemSetAccess(pub->vmm_ptrs[i], pub->vmm_slot_size, &access, 1);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuMemSetAccess slot %d: %s", i, cu_err_str(cr));
free_vmm_pool(pub);
return CUFRAMES_ERR_CUDA;
}
/* Export POSIX FD — будет shared с consumers через SCM_RIGHTS */
cr = cuMemExportToShareableHandle((void *)&pub->vmm_fds[i],
pub->vmm_handles[i],
CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR, 0);
if (cr != CUDA_SUCCESS) {
CUFRAMES_LOG_ERROR("cuMemExportToShareableHandle slot %d: %s",
i, cu_err_str(cr));
free_vmm_pool(pub);
return CUFRAMES_ERR_CUDA;
}
}
pub->has_vmm_pool = 1;
return CUFRAMES_OK;
}
static int register_external_pool(struct cuframes_publisher *pub,
void *const *ptrs, int32_t count,
size_t frame_size) {
if (count < 1 || count > CUFRAMES_MAX_RING) return CUFRAMES_ERR_INVALID_ARG;
pub->frame_size_bytes = frame_size;
pub->ring_size_actual = count;
pub->external_count = count;
cudaError_t cerr = cudaSetDevice(pub->cfg.cuda_device);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaSetDevice: %s", cudaGetErrorString(cerr));
return CUFRAMES_ERR_CUDA;
}
for (int i = 0; i < count; ++i) {
if (!ptrs[i]) return CUFRAMES_ERR_INVALID_ARG;
pub->cuda_ptrs[i] = ptrs[i];
pub->external_ptrs[i] = ptrs[i];
cerr = cudaIpcGetMemHandle(&pub->ipc_mem[i], ptrs[i]);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaIpcGetMemHandle on external ptr %p: %s",
ptrs[i], cudaGetErrorString(cerr));
return CUFRAMES_ERR_CUDA;
static void free_vmm_pool(struct cuframes_publisher *pub) {
for (int i = 0; i < CUFRAMES_MAX_RING; i++) {
if (pub->vmm_fds[i] >= 0) {
close(pub->vmm_fds[i]);
pub->vmm_fds[i] = -1;
}
if (pub->vmm_ptrs[i]) {
cuMemUnmap(pub->vmm_ptrs[i], pub->vmm_slot_size);
cuMemAddressFree(pub->vmm_ptrs[i], pub->vmm_slot_size);
pub->vmm_ptrs[i] = 0;
}
if (pub->vmm_handles[i]) {
cuMemRelease(pub->vmm_handles[i]);
pub->vmm_handles[i] = 0;
}
}
return CUFRAMES_OK;
}
static int create_event_handle(struct cuframes_publisher *pub) {
cudaError_t cerr = cudaEventCreateWithFlags(&pub->event,
cudaEventDisableTiming | cudaEventInterprocess);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaEventCreateWithFlags: %s",
cudaGetErrorString(cerr));
return CUFRAMES_ERR_CUDA;
}
return CUFRAMES_OK;
pub->has_vmm_pool = 0;
}
static int setup_shm(struct cuframes_publisher *pub) {
@@ -134,7 +200,8 @@ static int setup_shm(struct cuframes_publisher *pub) {
cuframes_shm_header_t tmp;
ssize_t rb = read(existing, &tmp, sizeof(tmp));
close(existing);
if (rb == (ssize_t)sizeof(tmp) && tmp.magic == CUFRAMES_MAGIC) {
if (rb == (ssize_t)sizeof(tmp) &&
(tmp.magic == CUFRAMES_MAGIC || tmp.magic == CUFRAMES_MAGIC_LEGACY)) {
if (cuframes_internal_pid_alive((pid_t)tmp.producer_pid)) {
CUFRAMES_LOG_ERROR("publisher with key=%s already running (pid %lu)",
pub->key, (unsigned long)tmp.producer_pid);
@@ -167,7 +234,7 @@ static int setup_shm(struct cuframes_publisher *pub) {
memset(pub->hdr, 0, sizeof(cuframes_shm_header_t));
pub->hdr->magic = CUFRAMES_MAGIC;
pub->hdr->proto_version = CUFRAMES_PROTOCOL_V1;
pub->hdr->proto_version = CUFRAMES_PROTOCOL_V4;
pub->hdr->lib_version_major = CUFRAMES_VERSION_MAJOR;
pub->hdr->lib_version_minor = CUFRAMES_VERSION_MINOR;
pub->hdr->lib_version_patch = CUFRAMES_VERSION_PATCH;
@@ -187,16 +254,11 @@ static int setup_shm(struct cuframes_publisher *pub) {
pub->hdr->meta.pitch_uv = puv;
pub->hdr->meta.frame_size_bytes = pub->frame_size_bytes;
/* Export event handle */
cudaError_t cerr = cudaIpcGetEventHandle(&pub->hdr->ipc_event_handle, pub->event);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaIpcGetEventHandle: %s", cudaGetErrorString(cerr));
return CUFRAMES_ERR_CUDA;
}
/* Fill slot descriptors */
/* v0.4: legacy event fields в header не используются (cuStreamSynchronize
* заменяет IPC events). Memzero выше — достаточно. */
/* Slot descriptors — mem_handle поле deprecated (передаётся через FDs),
* только seq atomic нужен. */
for (int i = 0; i < pub->ring_size_actual; ++i) {
pub->hdr->slots[i].mem_handle = pub->ipc_mem[i];
atomic_store_explicit(&pub->hdr->slots[i].seq, UINT64_MAX,
memory_order_release);
}
@@ -280,6 +342,7 @@ static int common_init(struct cuframes_publisher *pub,
pub->next_seq = 0;
pub->current_slot = -1;
pub->has_acquired = 0;
for (int i = 0; i < CUFRAMES_MAX_RING; i++) pub->vmm_fds[i] = -1;
pthread_mutex_init(&pub->state_mu, NULL);
return CUFRAMES_OK;
}
@@ -295,7 +358,6 @@ int cuframes_publisher_create(const cuframes_publisher_config_t *cfg,
common_init(pub, cfg);
if ((r = alloc_library_pool(pub)) != CUFRAMES_OK) goto fail;
if ((r = create_event_handle(pub)) != CUFRAMES_OK) goto fail;
if ((r = setup_shm(pub)) != CUFRAMES_OK) goto fail;
if ((r = setup_socket(pub)) != CUFRAMES_OK) goto fail;
@@ -307,7 +369,7 @@ int cuframes_publisher_create(const cuframes_publisher_config_t *cfg,
}
pub->accept_thread_alive = 1;
CUFRAMES_LOG_INFO("publisher '%s' ready (ring=%d, %dx%d, fmt=%d, lib-owned)",
CUFRAMES_LOG_INFO("publisher '%s' ready (ring=%d, %dx%d, fmt=%d, lib-owned, v0.4 VMM)",
pub->key, pub->ring_size_actual,
pub->cfg.width, pub->cfg.height, (int)pub->cfg.format);
*out = pub;
@@ -323,37 +385,12 @@ int cuframes_publisher_create_external(const cuframes_publisher_config_t *cfg,
int32_t ptr_count,
size_t frame_size,
cuframes_publisher_t **out) {
int r = validate_config(cfg);
if (r != CUFRAMES_OK) return r;
if (cfg->ownership != CUFRAMES_OWNERSHIP_EXTERNAL) return CUFRAMES_ERR_INVALID_ARG;
if (!cuda_ptrs || ptr_count < 1) return CUFRAMES_ERR_INVALID_ARG;
if (frame_size == 0) return CUFRAMES_ERR_INVALID_ARG;
struct cuframes_publisher *pub = calloc(1, sizeof(*pub));
if (!pub) return CUFRAMES_ERR_OUT_OF_MEMORY;
common_init(pub, cfg);
if ((r = register_external_pool(pub, cuda_ptrs, ptr_count, frame_size)) != CUFRAMES_OK)
goto fail;
if ((r = create_event_handle(pub)) != CUFRAMES_OK) goto fail;
if ((r = setup_shm(pub)) != CUFRAMES_OK) goto fail;
if ((r = setup_socket(pub)) != CUFRAMES_OK) goto fail;
pub->stop_flag = 0;
if (pthread_create(&pub->accept_thread, NULL, accept_thread_main, pub) != 0) {
r = CUFRAMES_ERR_INTERNAL;
goto fail;
}
pub->accept_thread_alive = 1;
CUFRAMES_LOG_INFO("publisher '%s' ready (external pool=%d, %dx%d, fmt=%d)",
pub->key, ptr_count,
pub->cfg.width, pub->cfg.height, (int)pub->cfg.format);
*out = pub;
return CUFRAMES_OK;
fail:
cuframes_publisher_destroy(pub);
return r;
/* v0.4: external ownership больше не поддерживается. VMM API требует
* cuMemCreate-allocated memory; existing cudaMalloc-pointers нельзя
* export'нуть как POSIX FD. Use LIBRARY ownership. */
(void)cfg; (void)cuda_ptrs; (void)ptr_count; (void)frame_size; (void)out;
CUFRAMES_LOG_ERROR("EXTERNAL ownership не поддерживается в v0.4 (VMM-only)");
return CUFRAMES_ERR_INVALID_ARG;
}
int cuframes_publisher_acquire(cuframes_publisher_t *pub, void **cuda_ptr_out) {
@@ -374,27 +411,24 @@ int cuframes_publisher_acquire(cuframes_publisher_t *pub, void **cuda_ptr_out) {
while (1) {
uint64_t ack = atomic_load_explicit(&pub->hdr->slots[slot].ack_bitmap,
memory_order_acquire);
/* Если slot ещё не публикован (seq == UINT64_MAX) — пропустить ack check */
uint64_t cur_seq = atomic_load_explicit(&pub->hdr->slots[slot].seq,
memory_order_acquire);
if (cur_seq == UINT64_MAX || (ack & bitmap) == bitmap) break;
if (deadline && cuframes_now_ns() > deadline) {
/* Mark slow subscriber dead и continue */
uint64_t missing = bitmap & ~ack;
CUFRAMES_LOG_WARN("strict-wait timeout, slow subscribers bitmap=0x%lx",
(unsigned long)missing);
/* clear missing subscribers — TODO: send unsubscribe in v0.2 */
atomic_fetch_and_explicit(&pub->hdr->subscriber_bitmap,
~missing, memory_order_release);
break;
}
struct timespec ts = {.tv_sec = 0, .tv_nsec = 100000}; /* 100µs */
struct timespec ts = {.tv_sec = 0, .tv_nsec = 100000};
nanosleep(&ts, NULL);
}
}
}
*cuda_ptr_out = pub->cuda_ptrs[slot];
*cuda_ptr_out = (void *)(uintptr_t)pub->vmm_ptrs[slot];
pub->current_slot = slot;
pub->has_acquired = 1;
return CUFRAMES_OK;
@@ -402,10 +436,14 @@ int cuframes_publisher_acquire(cuframes_publisher_t *pub, void **cuda_ptr_out) {
static int do_publish(cuframes_publisher_t *pub, int32_t slot,
void *stream, int64_t pts_ns) {
/* Record event on producer's stream */
cudaError_t cerr = cudaEventRecord(pub->event, (cudaStream_t)stream);
/* v0.4 — заменяет cudaEventRecord+IPC events на cuStreamSynchronize.
* Producer ждёт что stream flush'нулся (~1ms на 5090), потом publishes
* seq atomically. Consumer читает данные через DtoD memcpy без event
* wait — hardware coherence гарантирована на одном GPU. */
cudaError_t cerr = cudaStreamSynchronize((cudaStream_t)stream);
if (cerr != cudaSuccess) {
CUFRAMES_LOG_ERROR("cudaEventRecord: %s", cudaGetErrorString(cerr));
CUFRAMES_LOG_ERROR("cudaStreamSynchronize (slot %d): %s",
slot, cudaGetErrorString(cerr));
return CUFRAMES_ERR_CUDA;
}
@@ -438,44 +476,8 @@ int cuframes_publisher_publish(cuframes_publisher_t *pub, void *stream, int64_t
int cuframes_publisher_publish_external(cuframes_publisher_t *pub,
void *cuda_ptr, void *stream, int64_t pts_ns) {
if (!pub || !cuda_ptr) return CUFRAMES_ERR_INVALID_ARG;
if (pub->cfg.ownership != CUFRAMES_OWNERSHIP_EXTERNAL) return CUFRAMES_ERR_INVALID_ARG;
int32_t slot = -1;
for (int i = 0; i < pub->external_count; ++i) {
if (pub->external_ptrs[i] == cuda_ptr) { slot = i; break; }
}
if (slot < 0) {
CUFRAMES_LOG_ERROR("external pointer %p not registered", cuda_ptr);
return CUFRAMES_ERR_INVALID_ARG;
}
/* STRICT_WAIT — то же что в acquire, но per-publish */
if (pub->cfg.policy == CUFRAMES_POLICY_STRICT_WAIT) {
uint64_t bitmap = atomic_load_explicit(&pub->hdr->subscriber_bitmap,
memory_order_acquire);
if (bitmap != 0) {
int64_t deadline = pub->cfg.consumer_ack_timeout_ms > 0
? cuframes_now_ns() + (int64_t)pub->cfg.consumer_ack_timeout_ms * 1000000LL
: 0;
while (1) {
uint64_t ack = atomic_load_explicit(&pub->hdr->slots[slot].ack_bitmap,
memory_order_acquire);
uint64_t cur_seq = atomic_load_explicit(&pub->hdr->slots[slot].seq,
memory_order_acquire);
if (cur_seq == UINT64_MAX || (ack & bitmap) == bitmap) break;
if (deadline && cuframes_now_ns() > deadline) {
uint64_t missing = bitmap & ~ack;
atomic_fetch_and_explicit(&pub->hdr->subscriber_bitmap,
~missing, memory_order_release);
break;
}
struct timespec ts = {.tv_sec = 0, .tv_nsec = 100000};
nanosleep(&ts, NULL);
}
}
}
return do_publish(pub, slot, stream, pts_ns);
(void)pub; (void)cuda_ptr; (void)stream; (void)pts_ns;
return CUFRAMES_ERR_INVALID_ARG; /* v0.4 — нет external mode */
}
int cuframes_publisher_destroy(cuframes_publisher_t *pub) {
@@ -497,13 +499,16 @@ int cuframes_publisher_destroy(cuframes_publisher_t *pub) {
pub->accept_thread_alive = 0;
}
/* Free CUDA */
if (pub->cfg.ownership == CUFRAMES_OWNERSHIP_LIBRARY) {
for (int i = 0; i < pub->ring_size_actual; ++i) {
if (pub->cuda_ptrs[i]) cudaFree(pub->cuda_ptrs[i]);
}
/* Free VMM */
if (pub->has_vmm_pool) {
free_vmm_pool(pub);
}
/* Packet ring cleanup (если активирован) */
if (pub->has_pkt_ring) {
cuframes_internal_pkt_ring_destroy(&pub->pkt_ring);
pub->has_pkt_ring = 0;
}
if (pub->event) cudaEventDestroy(pub->event);
/* Unlink resources */
if (pub->hdr) {
@@ -523,8 +528,83 @@ int cuframes_publisher_destroy(cuframes_publisher_t *pub) {
return CUFRAMES_OK;
}
/* ─────────────────────────────────────────────────────────────────────── */
/* v0.2 — encoded packet ring API (см. docs/protocol.md §10) */
/* ─────────────────────────────────────────────────────────────────────── */
int cuframes_publisher_enable_packets(cuframes_publisher_t *pub,
const cuframes_packet_ring_options_t *opts) {
if (!pub) return CUFRAMES_ERR_INVALID_ARG;
if (pub->has_pkt_ring) return CUFRAMES_ERR_ALREADY_EXISTS;
uint32_t slots = opts && opts->ring_slots ? opts->ring_slots
: CUFRAMES_PKT_DEFAULT_SLOTS;
uint32_t data_size = opts && opts->data_size ? opts->data_size
: CUFRAMES_PKT_DEFAULT_DATA_SIZE;
uint32_t max_pkt = opts && opts->max_packet_size ? opts->max_packet_size
: CUFRAMES_PKT_DEFAULT_MAX_SIZE;
uint32_t codec_id = opts ? opts->codec_id : 0;
if (max_pkt > data_size) {
CUFRAMES_LOG_ERROR("max_packet_size (%u) > data_size (%u)", max_pkt, data_size);
return CUFRAMES_ERR_INVALID_ARG;
}
int r = cuframes_internal_pkt_ring_create(pub->key, slots, data_size,
codec_id, &pub->pkt_ring);
if (r != CUFRAMES_OK) return r;
pub->has_pkt_ring = 1;
pub->max_packet_size = max_pkt;
/* v0.4 frame header proto не bumped из-за packet ring — оба коэкзистируют. */
return CUFRAMES_OK;
}
int cuframes_publisher_set_codec_extradata(cuframes_publisher_t *pub,
const void *extradata, size_t size) {
if (!pub) return CUFRAMES_ERR_INVALID_ARG;
if (!pub->has_pkt_ring) return CUFRAMES_ERR_NO_PACKET_RING;
return cuframes_internal_pkt_ring_set_extradata(&pub->pkt_ring,
extradata, size);
}
int cuframes_publisher_publish_packet(cuframes_publisher_t *pub,
const void *data, size_t size,
int64_t pts_ns, int64_t dts_ns,
uint32_t flags) {
if (!pub) return CUFRAMES_ERR_INVALID_ARG;
if (!pub->has_pkt_ring) return CUFRAMES_ERR_NO_PACKET_RING;
if (size > pub->max_packet_size) return CUFRAMES_ERR_PACKET_OVERSIZED;
return cuframes_internal_pkt_ring_publish(&pub->pkt_ring, data, size,
pts_ns, dts_ns, flags);
}
/* ─── Accept thread + handshake ──────────────────────────────────────── */
struct sub_monitor_args {
struct cuframes_publisher *pub;
int fd;
uint32_t bit;
};
static void *subscriber_monitor_thread(void *arg) {
struct sub_monitor_args *m = (struct sub_monitor_args *)arg;
char buf[64];
while (1) {
ssize_t n = recv(m->fd, buf, sizeof(buf), 0);
if (n <= 0) {
atomic_fetch_and_explicit(&m->pub->hdr->subscriber_bitmap,
~(1ULL << m->bit), memory_order_release);
atomic_store_explicit(&m->pub->hdr->subscribers[m->bit].state, 0,
memory_order_release);
close(m->fd);
CUFRAMES_LOG_INFO("subscriber bit=%u disconnected — freed", m->bit);
free(m);
return NULL;
}
}
}
static void *accept_thread_main(void *arg) {
struct cuframes_publisher *pub = (struct cuframes_publisher *)arg;
while (!pub->stop_flag) {
@@ -537,21 +617,16 @@ static void *accept_thread_main(void *arg) {
CUFRAMES_LOG_WARN("accept: %s", strerror(errno));
continue;
}
/* Synchronous handshake — после ответа socket остаётся открытым для
* lifetime signals (SHUTDOWN, PING). Close на error. */
int r = handshake_subscriber(pub, client);
if (r != CUFRAMES_OK) {
close(client);
}
/* TODO v0.2: track client fds для broadcast SHUTDOWN. Сейчас clients
* сами detect socket EOF при publisher_destroy через shutdown(). */
}
return NULL;
}
static int allocate_subscriber_bit(struct cuframes_publisher *pub,
const char *name, uint32_t *bit_out) {
/* Bit 0 reserved (sentinel). Bits 1..31. */
pthread_mutex_lock(&pub->state_mu);
for (uint32_t bit = 1; bit < CUFRAMES_MAX_SUBSCRIBERS; ++bit) {
uint64_t state = atomic_load_explicit(&pub->hdr->subscribers[bit].state,
@@ -571,7 +646,6 @@ static int allocate_subscriber_bit(struct cuframes_publisher *pub,
pthread_mutex_unlock(&pub->state_mu);
return CUFRAMES_OK;
}
/* Check for name collision */
if (name && state >= 2 &&
strncmp(pub->hdr->subscribers[bit].consumer_name, name,
sizeof(pub->hdr->subscribers[bit].consumer_name)) == 0) {
@@ -598,7 +672,6 @@ static int handshake_subscriber(struct cuframes_publisher *pub, int client_fd) {
return CUFRAMES_ERR_PROTOCOL;
}
/* Parse HELLO_REQ: proto_version + name_len + name + cuda_device + mode */
if (plen < sizeof(cuframes_msg_hello_req_t) + 20) return CUFRAMES_ERR_PROTOCOL;
cuframes_msg_hello_req_t *hreq = (cuframes_msg_hello_req_t *)buf;
uint32_t want_proto = hreq->proto_version;
@@ -608,18 +681,18 @@ static int handshake_subscriber(struct cuframes_publisher *pub, int client_fd) {
char name[32] = {0};
memcpy(name, buf + sizeof(*hreq), name_len);
int proto_match = (want_proto == CUFRAMES_PROTOCOL_V1);
/* v0.4 принимает только V4 consumers. Старые v0.3 fail здесь cleanly. */
int proto_match = (want_proto == CUFRAMES_PROTOCOL_V4);
/* Send HELLO_RESP */
uint8_t resp_buf[CUFRAMES_MAX_MSG_PAYLOAD];
cuframes_msg_hello_resp_t *resp = (cuframes_msg_hello_resp_t *)resp_buf;
memset(resp, 0, sizeof(*resp));
resp->result = proto_match ? CUFRAMES_OK : CUFRAMES_ERR_PROTOCOL;
resp->proto_version_actual = CUFRAMES_PROTOCOL_V1;
resp->proto_version_actual = CUFRAMES_PROTOCOL_V4;
resp->ring_size = (uint32_t)pub->ring_size_actual;
resp->ownership_mode = (uint32_t)pub->cfg.ownership;
resp->meta = pub->hdr->meta;
/* shm_path */
int slen = snprintf((char *)(resp_buf + sizeof(*resp)),
sizeof(resp_buf) - sizeof(*resp) - 12,
"%s", pub->shm_name);
@@ -632,7 +705,11 @@ static int handshake_subscriber(struct cuframes_publisher *pub, int client_fd) {
CUFRAMES_LOG_WARN("send HELLO_RESP: %s", cuframes_strerror(r));
return r;
}
if (!proto_match) return CUFRAMES_ERR_PROTOCOL;
if (!proto_match) {
CUFRAMES_LOG_WARN("subscriber proto v%u rejected (want v%u)",
want_proto, CUFRAMES_PROTOCOL_V4);
return CUFRAMES_ERR_PROTOCOL;
}
/* recv SUBSCRIBE_REQ */
plen = sizeof(buf);
@@ -640,11 +717,9 @@ static int handshake_subscriber(struct cuframes_publisher *pub, int client_fd) {
if (r != CUFRAMES_OK) return r;
if (mtype != CUFRAMES_MSG_SUBSCRIBE_REQ) return CUFRAMES_ERR_PROTOCOL;
/* Allocate subscriber bit */
uint32_t bit = 0;
int alloc_r = allocate_subscriber_bit(pub, name, &bit);
/* Send SUBSCRIBE_RESP */
cuframes_msg_subscribe_resp_t sresp = {0};
sresp.result = alloc_r;
sresp.assigned_bit = bit;
@@ -655,13 +730,42 @@ static int handshake_subscriber(struct cuframes_publisher *pub, int client_fd) {
&sresp, sizeof(sresp));
if (r != CUFRAMES_OK || alloc_r != CUFRAMES_OK) return r ? r : alloc_r;
/* Activate subscriber slot */
/* v0.4 — отправить VMM_FDS с N posix FDs через SCM_RIGHTS */
cuframes_msg_vmm_fds_t vmm_payload = {0};
vmm_payload.slot_size_bytes = pub->vmm_slot_size;
vmm_payload.fd_count = (uint32_t)pub->ring_size_actual;
r = cuframes_internal_send_msg_with_fds(client_fd, CUFRAMES_MSG_VMM_FDS,
&vmm_payload, sizeof(vmm_payload),
pub->vmm_fds,
(uint32_t)pub->ring_size_actual);
if (r != CUFRAMES_OK) {
CUFRAMES_LOG_WARN("send VMM_FDS: %s", cuframes_strerror(r));
/* roll back bit allocation */
atomic_fetch_and_explicit(&pub->hdr->subscriber_bitmap,
~(1ULL << bit), memory_order_release);
atomic_store_explicit(&pub->hdr->subscribers[bit].state, 0,
memory_order_release);
return r;
}
atomic_store_explicit(&pub->hdr->subscribers[bit].state, 2,
memory_order_release);
CUFRAMES_LOG_INFO("subscriber '%s' connected (bit=%u)", name, bit);
CUFRAMES_LOG_INFO("subscriber '%s' connected (bit=%u, %d VMM FDs)",
name, bit, pub->ring_size_actual);
/* TODO v0.2: spawn per-client thread для liveness/PING/UNSUBSCRIBE.
* Сейчас socket остаётся открытым на heap'е до publisher_destroy. */
/* Spawn monitor thread */
struct sub_monitor_args *m = malloc(sizeof(*m));
if (!m) return CUFRAMES_OK;
m->pub = pub;
m->fd = client_fd;
m->bit = bit;
pthread_t monitor_tid;
if (pthread_create(&monitor_tid, NULL, subscriber_monitor_thread, m) != 0) {
CUFRAMES_LOG_WARN("monitor pthread_create fail — bit %u may leak", bit);
free(m);
} else {
pthread_detach(monitor_tid);
}
return CUFRAMES_OK;
}
+120
View File
@@ -3,7 +3,9 @@
#include "internal.h"
#include <errno.h>
#include <poll.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <unistd.h>
/* Read exactly N bytes from socket, with poll-based timeout. */
@@ -97,3 +99,121 @@ int cuframes_internal_recv_msg(int fd, uint32_t *msg_type_out,
if (payload_len_inout) *payload_len_inout = h.payload_length;
return CUFRAMES_OK;
}
/* v0.4 — send TLV msg + N FDs через SCM_RIGHTS. Один sendmsg(): header+payload
* в iovec, FDs в control. Header.payload_length описывает ТОЛЬКО payload bytes,
* FDs приходят out-of-band. */
int cuframes_internal_send_msg_with_fds(int sock_fd, uint32_t msg_type,
const void *payload, uint32_t payload_len,
const int *fds, uint32_t fd_count) {
if (payload_len > CUFRAMES_MAX_MSG_PAYLOAD) return CUFRAMES_ERR_INVALID_ARG;
if (fd_count > 0 && !fds) return CUFRAMES_ERR_INVALID_ARG;
cuframes_msg_header_t h = {.msg_type = msg_type, .payload_length = payload_len};
struct iovec iov[2];
iov[0].iov_base = &h; iov[0].iov_len = sizeof(h);
iov[1].iov_base = (void *)payload; iov[1].iov_len = payload_len;
struct msghdr msg = {0};
msg.msg_iov = iov;
msg.msg_iovlen = (payload_len > 0 && payload) ? 2 : 1;
char ctrl_buf[CMSG_SPACE(sizeof(int) * 64)] = {0};
if (fd_count > 0) {
if (fd_count > 64) return CUFRAMES_ERR_INVALID_ARG;
msg.msg_control = ctrl_buf;
msg.msg_controllen = CMSG_SPACE(sizeof(int) * fd_count);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int) * fd_count);
memcpy(CMSG_DATA(cmsg), fds, sizeof(int) * fd_count);
}
ssize_t n = sendmsg(sock_fd, &msg, MSG_NOSIGNAL);
if (n < 0) {
if (errno == EPIPE) return CUFRAMES_ERR_DISCONNECTED;
return CUFRAMES_ERR_IO;
}
/* Partial send rare для small payload — но обработаем gracefully */
size_t want = sizeof(h) + payload_len;
if ((size_t)n < want) {
return send_all(sock_fd, (uint8_t *)iov[0].iov_base + n,
want - (size_t)n);
}
return CUFRAMES_OK;
}
int cuframes_internal_recv_msg_with_fds(int sock_fd, uint32_t *msg_type_out,
void *payload, uint32_t *payload_len_inout,
int *fds_out, uint32_t *fd_count_inout,
int32_t timeout_ms) {
/* Poll первым делом — recvmsg блокирующий, иначе тайм-аут не сработает. */
if (timeout_ms >= 0) {
struct pollfd pfd = {.fd = sock_fd, .events = POLLIN};
int pr = poll(&pfd, 1, timeout_ms);
if (pr == 0) return CUFRAMES_ERR_TIMEOUT;
if (pr < 0) return CUFRAMES_ERR_IO;
}
cuframes_msg_header_t h;
struct iovec iov[2];
iov[0].iov_base = &h; iov[0].iov_len = sizeof(h);
iov[1].iov_base = payload; iov[1].iov_len = (payload && payload_len_inout) ? *payload_len_inout : 0;
uint32_t want_fds = (fd_count_inout && fds_out) ? *fd_count_inout : 0;
char ctrl_buf[CMSG_SPACE(sizeof(int) * 64)] = {0};
struct msghdr msg = {0};
msg.msg_iov = iov;
msg.msg_iovlen = (iov[1].iov_len > 0) ? 2 : 1;
msg.msg_control = ctrl_buf;
msg.msg_controllen = sizeof(ctrl_buf);
ssize_t n = recvmsg(sock_fd, &msg, 0);
if (n == 0) return CUFRAMES_ERR_DISCONNECTED;
if (n < 0) return CUFRAMES_ERR_IO;
if ((size_t)n < sizeof(h)) return CUFRAMES_ERR_PROTOCOL;
if (msg_type_out) *msg_type_out = h.msg_type;
if (h.payload_length > CUFRAMES_MAX_MSG_PAYLOAD) return CUFRAMES_ERR_PROTOCOL;
/* Если recvmsg вернул меньше payload_length — добираем через recv_all */
size_t got_payload = (size_t)n - sizeof(h);
if (h.payload_length > 0) {
if (!payload || !payload_len_inout || *payload_len_inout < h.payload_length) {
return CUFRAMES_ERR_INVALID_ARG;
}
if (got_payload < h.payload_length) {
int r = recv_all(sock_fd, (uint8_t *)payload + got_payload,
h.payload_length - got_payload, timeout_ms);
if (r != CUFRAMES_OK) return r;
}
*payload_len_inout = h.payload_length;
} else if (payload_len_inout) {
*payload_len_inout = 0;
}
/* Parse SCM_RIGHTS FDs */
uint32_t got_fds = 0;
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
for (; cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) {
if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) {
size_t blob = cmsg->cmsg_len - CMSG_LEN(0);
uint32_t n_fds = (uint32_t)(blob / sizeof(int));
if (got_fds + n_fds > want_fds) {
/* Close excess FDs чтобы не утекли */
for (uint32_t i = 0; i < n_fds; i++) {
int f;
memcpy(&f, CMSG_DATA(cmsg) + i * sizeof(int), sizeof(int));
close(f);
}
continue;
}
memcpy(fds_out + got_fds, CMSG_DATA(cmsg), blob);
got_fds += n_fds;
}
}
if (fd_count_inout) *fd_count_inout = got_fds;
return CUFRAMES_OK;
}
+13
View File
@@ -32,6 +32,10 @@ const char *cuframes_strerror(int err) {
case CUFRAMES_ERR_FORMAT: return "unsupported format or size mismatch";
case CUFRAMES_ERR_WOULD_BLOCK: return "would block";
case CUFRAMES_ERR_TOO_MANY: return "too many subscribers (max 32)";
case CUFRAMES_ERR_PACKET_OVERSIZED: return "packet exceeds max_packet_size";
case CUFRAMES_ERR_NO_PACKET_RING: return "publisher has no packet ring";
case CUFRAMES_ERR_NO_CODEC_PARAMS: return "codec extradata not set by publisher";
case CUFRAMES_ERR_PACKET_OVERRUN: return "packet ring overrun — resync on keyframe";
case CUFRAMES_ERR_INTERNAL: return "internal error (please report)";
default: return "unknown error";
}
@@ -83,6 +87,15 @@ int cuframes_internal_shm_name(const char *key, char *out, size_t out_size) {
return CUFRAMES_OK;
}
int cuframes_internal_pkt_shm_name(const char *key, char *out, size_t out_size) {
int r = cuframes_internal_validate_key(key);
if (r != CUFRAMES_OK) return r;
int n = snprintf(out, out_size, "%s%s%s",
CUFRAMES_SHM_PREFIX, key, CUFRAMES_PKT_SHM_SUFFIX);
if (n < 0 || (size_t)n >= out_size) return CUFRAMES_ERR_INVALID_ARG;
return CUFRAMES_OK;
}
int cuframes_internal_ensure_runtime_dir(void) {
if (mkdir(CUFRAMES_RUNTIME_DIR, 0755) == 0) return CUFRAMES_OK;
if (errno == EEXIST) return CUFRAMES_OK;
+8
View File
@@ -22,3 +22,11 @@ target_include_directories(test_stress PRIVATE
${CMAKE_SOURCE_DIR}/include)
add_test(NAME stress_4consumer COMMAND test_stress)
set_tests_properties(stress_4consumer PROPERTIES TIMEOUT 120)
# v0.2 — packet ring tests (host-only, без CUDA в test-коде)
add_executable(test_packet_ring test_packet_ring.c)
target_link_libraries(test_packet_ring PRIVATE cuframes)
target_include_directories(test_packet_ring PRIVATE
${CMAKE_SOURCE_DIR}/include)
add_test(NAME packet_ring_basic COMMAND test_packet_ring)
set_tests_properties(packet_ring_basic PROPERTIES TIMEOUT 120)
+280
View File
@@ -0,0 +1,280 @@
/* Stress test для encoded packet ring (v0.2).
*
* Сценарии:
* 1) Normal flow: 1 publisher × 1 subscriber × 2000 packets, varied sizes,
* каждые 30 packets — KEY flag (имитация GOP). Subscriber проверяет:
* - монотонные seq (без пропусков в этом тесте — fast consumer)
* - data integrity через checksum (XOR fold)
* - PTS/DTS monotonic, KEY flag доходит
* 2) Slow subscriber: publisher шлёт быстрее чем subscriber читает →
* должен случиться OVERRUN, library resync'нет на keyframe.
* 3) Cleanup: после exit нет leaked SHM в /dev/shm.
*
* Без CUDA-зависимостей (packets host-side).
*/
#include <cuframes/cuframes.h>
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#define KEY "test_pkt_ring"
#define TOTAL_PACKETS 2000
#define GOP_SIZE 30
#define SMALL_PKT 4096
#define LARGE_PKT (256 * 1024)
#define CHECK(call) do { int _r = (call); if (_r != 0) { \
fprintf(stderr, "FAIL %s:%d (rc=%d): %s\n", __FILE__, __LINE__, _r, \
cuframes_strerror(_r)); exit(2); } } while (0)
#define EXPECT_TRUE(cond) do { if (!(cond)) { \
fprintf(stderr, "EXPECT_TRUE failed at %s:%d: %s\n", \
__FILE__, __LINE__, #cond); exit(2); } } while (0)
/* Сгенерировать payload: первые 8 байт = seq (little-endian), остальное pattern. */
static void gen_payload(uint8_t *buf, size_t size, uint64_t seq) {
memcpy(buf, &seq, sizeof(seq));
for (size_t i = sizeof(seq); i < size; ++i) {
buf[i] = (uint8_t)((seq + i) & 0xFF);
}
}
/* Verify payload matches seq. Возвращает 0 если ok. */
static int verify_payload(const uint8_t *buf, size_t size, uint64_t expected_seq) {
uint64_t seq_in_buf;
if (size < sizeof(seq_in_buf)) return -1;
memcpy(&seq_in_buf, buf, sizeof(seq_in_buf));
if (seq_in_buf != expected_seq) return -2;
for (size_t i = sizeof(seq_in_buf); i < size; ++i) {
if (buf[i] != (uint8_t)((expected_seq + i) & 0xFF)) return -3;
}
return 0;
}
static cuframes_publisher_t *make_publisher(void) {
cuframes_publisher_config_t cfg = {0};
cfg.key = KEY;
cfg.width = 320;
cfg.height = 240;
cfg.format = CUFRAMES_FORMAT_NV12;
cfg.ownership = CUFRAMES_OWNERSHIP_LIBRARY;
cfg.ring_size = 2;
cfg.policy = CUFRAMES_POLICY_DROP_OLDEST;
cfg.cuda_device = 0;
cuframes_publisher_t *pub = NULL;
CHECK(cuframes_publisher_create(&cfg, &pub));
cuframes_packet_ring_options_t pkt_opts = {0};
pkt_opts.codec_id = 27; /* AV_CODEC_ID_H264 */
pkt_opts.ring_slots = 64;
pkt_opts.data_size = 8 * 1024 * 1024;
pkt_opts.max_packet_size = LARGE_PKT * 2;
CHECK(cuframes_publisher_enable_packets(pub, &pkt_opts));
/* Fake SPS/PPS — 16 байт */
uint8_t extradata[16];
for (int i = 0; i < 16; ++i) extradata[i] = (uint8_t)(0xAA + i);
CHECK(cuframes_publisher_set_codec_extradata(pub, extradata, sizeof(extradata)));
return pub;
}
/* Subscriber-процесс. read_delay_us позволяет имитировать slow consumer. */
static int run_subscriber(int read_delay_us, int *out_received, int *out_overruns,
int *out_first_key_seq) {
/* Wait чтобы publisher успел создать SHM */
usleep(100 * 1000);
cuframes_subscriber_config_t cfg = {0};
cfg.key = KEY;
cfg.mode = CUFRAMES_MODE_NEWEST_ONLY;
cfg.cuda_device = 0;
cfg.connect_timeout_ms = 5000;
cuframes_subscriber_t *sub = NULL;
CHECK(cuframes_subscriber_create(&cfg, &sub));
CHECK(cuframes_subscriber_enable_packets(sub));
/* Verify codec params */
uint32_t codec_id = 0;
const void *extradata = NULL;
size_t extradata_sz = 0;
int r = cuframes_subscriber_get_codec_params(sub, &codec_id, &extradata, &extradata_sz);
EXPECT_TRUE(r == CUFRAMES_OK);
EXPECT_TRUE(codec_id == 27);
EXPECT_TRUE(extradata_sz == 16);
int received = 0;
int overruns = 0;
int first_key_seq = -1;
int64_t last_pts = -1;
int data_errors = 0;
/* Run на ~30s или до того как publisher закончит. */
time_t start = time(NULL);
while (time(NULL) - start < 30) {
cuframes_packet_t *pkt = NULL;
int rc = cuframes_subscriber_next_packet(sub, &pkt, 500);
if (rc == CUFRAMES_ERR_TIMEOUT || rc == CUFRAMES_ERR_WOULD_BLOCK) {
if (received >= TOTAL_PACKETS / 2) break; /* достаточно для теста */
continue;
}
if (rc == CUFRAMES_ERR_DISCONNECTED) break;
if (rc == CUFRAMES_ERR_PACKET_OVERRUN) {
overruns++;
continue; /* library resync'нет на next call */
}
if (rc != CUFRAMES_OK) {
fprintf(stderr, "next_packet rc=%d (%s)\n", rc, cuframes_strerror(rc));
break;
}
const uint8_t *data = (const uint8_t *)cuframes_packet_data(pkt);
size_t size = cuframes_packet_size(pkt);
int64_t pts = cuframes_packet_pts(pkt);
uint32_t flags = cuframes_packet_flags(pkt);
uint64_t seq = cuframes_packet_seq(pkt);
if (verify_payload(data, size, seq) != 0) {
data_errors++;
}
if ((flags & CUFRAMES_PKT_FLAG_KEY) && first_key_seq < 0) {
first_key_seq = (int)seq;
}
if (pts <= last_pts && last_pts >= 0) {
fprintf(stderr, "PTS не монотонно: %ld <= %ld (seq=%lu)\n",
pts, last_pts, seq);
}
last_pts = pts;
received++;
cuframes_subscriber_release_packet(sub, pkt);
if (read_delay_us > 0) usleep(read_delay_us);
}
EXPECT_TRUE(data_errors == 0);
cuframes_subscriber_destroy(sub);
*out_received = received;
*out_overruns = overruns;
*out_first_key_seq = first_key_seq;
return 0;
}
static void publisher_loop(int total_packets, int inter_packet_us) {
cuframes_publisher_t *pub = make_publisher();
/* Buffer pre-alloc — max size */
uint8_t *buf = (uint8_t *)malloc(LARGE_PKT);
EXPECT_TRUE(buf != NULL);
for (int i = 0; i < total_packets; ++i) {
int is_key = (i % GOP_SIZE == 0);
size_t size = is_key ? LARGE_PKT : SMALL_PKT + (i % 8) * 1024;
gen_payload(buf, size, (uint64_t)i);
int64_t pts_ns = (int64_t)i * 33333333LL; /* ~30 fps */
uint32_t flags = is_key ? CUFRAMES_PKT_FLAG_KEY : 0;
int rc = cuframes_publisher_publish_packet(pub, buf, size,
pts_ns, pts_ns, flags);
if (rc != CUFRAMES_OK) {
fprintf(stderr, "publish rc=%d size=%zu\n", rc, size);
}
if (inter_packet_us > 0) usleep(inter_packet_us);
}
free(buf);
cuframes_publisher_destroy(pub);
}
static int check_no_leaked_shm(void) {
int fail = 0;
char path[256];
snprintf(path, sizeof(path), "/dev/shm/cuframes-%s", KEY);
if (access(path, F_OK) == 0) {
fprintf(stderr, "LEAKED %s\n", path);
fail = 1;
}
snprintf(path, sizeof(path), "/dev/shm/cuframes-%s-packets", KEY);
if (access(path, F_OK) == 0) {
fprintf(stderr, "LEAKED %s\n", path);
fail = 1;
}
return fail;
}
static int scenario_normal_flow(void) {
fprintf(stderr, "[scenario 1] normal flow — fast consumer\n");
pid_t pid = fork();
EXPECT_TRUE(pid >= 0);
if (pid == 0) {
/* child = subscriber */
int received = 0, overruns = 0, first_key = -1;
run_subscriber(0, &received, &overruns, &first_key);
fprintf(stderr, " consumer: received=%d overruns=%d first_key_seq=%d\n",
received, overruns, first_key);
EXPECT_TRUE(received >= TOTAL_PACKETS / 2);
EXPECT_TRUE(overruns == 0);
EXPECT_TRUE(first_key >= 0);
exit(0);
}
/* parent = publisher (медленнее чем consumer) */
publisher_loop(TOTAL_PACKETS, 1000); /* 1ms между packets = 1000 fps */
int status = 0;
waitpid(pid, &status, 0);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
return 0;
}
static int scenario_slow_consumer(void) {
fprintf(stderr, "[scenario 2] slow consumer — must hit OVERRUN + resync\n");
pid_t pid = fork();
EXPECT_TRUE(pid >= 0);
if (pid == 0) {
/* child = очень медленный subscriber */
int received = 0, overruns = 0, first_key = -1;
run_subscriber(10 * 1000, &received, &overruns, &first_key); /* 10ms */
fprintf(stderr, " consumer: received=%d overruns=%d first_key_seq=%d\n",
received, overruns, first_key);
/* Должны быть overruns поскольку publisher faster */
EXPECT_TRUE(overruns > 0);
/* И всё-таки что-то получили (resync работает) */
EXPECT_TRUE(received > 10);
exit(0);
}
/* publisher fast — 200 fps */
publisher_loop(TOTAL_PACKETS, 5 * 1000);
int status = 0;
waitpid(pid, &status, 0);
EXPECT_TRUE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
return 0;
}
int main(void) {
signal(SIGPIPE, SIG_IGN);
scenario_normal_flow();
/* Ensure clean inter-test state */
usleep(200 * 1000);
if (check_no_leaked_shm()) exit(2);
scenario_slow_consumer();
usleep(200 * 1000);
if (check_no_leaked_shm()) exit(2);
fprintf(stderr, "OK — all scenarios passed\n");
return 0;
}
+7
View File
@@ -0,0 +1,7 @@
build/
dist/
*.egg-info/
__pycache__/
*.pyc
*.so
.pytest_cache/
+52
View File
@@ -0,0 +1,52 @@
# Python bindings for cuframes — pybind11 module.
#
# Buildup: используется как subdirectory из root CMakeLists.txt при
# BUILD_PYTHON_BINDINGS=ON, либо standalone через scikit-build-core
# (см. pyproject.toml).
#
# Output: единый shared module `_native.so` который импортируется из
# Python package `cuframes` (cuframes/__init__.py re-export'ит публичный API).
include(FetchContent)
# pybind11 — header-only + helper functions. FetchContent чтобы не требовать
# system install; pinned tag для воспроизводимых билдов.
FetchContent_Declare(
pybind11
GIT_REPOSITORY https://github.com/pybind/pybind11.git
GIT_TAG v2.13.6
GIT_SHALLOW TRUE
)
FetchContent_MakeAvailable(pybind11)
pybind11_add_module(_native MODULE
src/_native.cpp
)
target_include_directories(_native PRIVATE
${PROJECT_SOURCE_DIR}/include
)
target_link_libraries(_native PRIVATE
cuframes # imported target из libcuframes/CMakeLists.txt
)
# Версия модуля соответствует libcuframes (см. cuframes.h)
target_compile_definitions(_native PRIVATE
CUFRAMES_PY_BINDING_VERSION="${PROJECT_VERSION}"
)
set_target_properties(_native PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_VISIBILITY_PRESET hidden
INTERPROCEDURAL_OPTIMIZATION TRUE
)
# При scikit-build-core билде модуль попадает в wheel рядом с Python-исходниками
# пакета. При standalone CMake — устанавливается в site-packages по умолчанию.
if(SKBUILD)
install(TARGETS _native DESTINATION cuframes)
else()
install(TARGETS _native LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/cuframes)
endif()
+53
View File
@@ -0,0 +1,53 @@
# cuframes — Python bindings
Status: **WIP** (Phase 0 skeleton — issue [gx/cuframes#6](http://server:3000/gx/cuframes/issues/6))
Это пакет Python-обёрток над `libcuframes` (C ABI). Цель — позволить
downstream ML/CV пайплайнам (yolo-world-detector, zone-motion, custom
скриптам) подписываться на cuframes без CPU round-trip: получать NV12
frames прямо как CUDA pointer / `torch.Tensor` (DLPack export, zero-copy).
## Текущий статус (что уже работает в этом skeleton)
- Module import: `import cuframes` загружает `_native.so`
- Версия: `cuframes.version_string()`, `cuframes.protocol_version()`
- Enums: `PixelFormat`, `SubscriberMode`
- Иерархия исключений: `CuframesError` + 8 subclasses (publisher gone,
frame timeout, device lost, и т. д.)
## Что в работе (см. tasks #198-#202)
- [ ] `CuframesSubscriber` + `CuframesFrame` lifecycle
- [ ] DLPack export → `torch.from_dlpack`, `cupy.from_dlpack`
- [ ] Context manager (`with cuframes.subscribe(key) as sub:`)
- [ ] Per-subscriber CUDA stream
- [ ] Health/stats properties (`ring_occupancy`, `drop_count`)
- [ ] Thread-safety contract документация
## Build (dev)
Standalone wheel:
```bash
cd python/
pip install -e . --no-build-isolation
```
Через корневой CMake-проект (вместе с libcuframes):
```bash
cmake -B build -DBUILD_PYTHON_BINDINGS=ON
cmake --build build -j
```
## Зависимости
- `libcuframes` ≥ 0.4 (линкуется из соседнего CMake target)
- CUDA Toolkit 12+
- `pybind11` 2.13+ (берётся через FetchContent при CMake-сборке)
- Python 3.10+
- Опционально: `torch>=2.4` или `cupy-cuda12x>=13` для DLPack-потребителей
## Лицензия
LGPL-2.1+ (как у libcuframes).
+77
View File
@@ -0,0 +1,77 @@
"""cuframes — zero-copy CUDA frame sharing.
Python bindings to libcuframes. См. docs/python.md (т.б.д.) для
архитектуры, threading контракта и примеров интеграции с PyTorch/CuPy.
Пример (subscriber-side):
import cuframes
with cuframes.subscribe("cam-parking",
consumer_name="yolo-world",
connect_timeout_ms=5000) as sub:
# next_frame returns CuframesFrame — context manager
with sub.next_frame(timeout_ms=1000) as frame:
print(frame.cuda_ptr, frame.width, frame.height,
frame.pitch_y, frame.seq, frame.pts_ns)
# DLPack export — в task #199, пока через cuda-python:
# cuda_arr = cuda.from_pointer(frame.cuda_ptr, ...)
Reconnect-loop пример:
while True:
try:
with cuframes.subscribe("cam-parking", connect_timeout_ms=5000) as sub:
while True:
try:
with sub.next_frame(timeout_ms=1000) as frame:
process(frame)
except cuframes.CuframesFrameTimeout:
continue # просто нет новых кадров
except cuframes.CuframesPublisherGone:
time.sleep(1) # publisher restart — переподписываемся
"""
from ._native import (
# Метаданные
version_string,
protocol_version,
# Enums
PixelFormat,
SubscriberMode,
# Core API
CuframesSubscriber,
CuframesFrame,
subscribe,
# Error taxonomy
CuframesError,
CuframesPublisherGone,
CuframesFrameTimeout,
CuframesDeviceLost,
CuframesShmError,
CuframesProtocolMismatch,
CuframesInvalidArgument,
CuframesOutOfMemory,
CuframesInternal,
)
__version__ = version_string()
__all__ = [
"version_string",
"protocol_version",
"PixelFormat",
"SubscriberMode",
"CuframesSubscriber",
"CuframesFrame",
"subscribe",
"CuframesError",
"CuframesPublisherGone",
"CuframesFrameTimeout",
"CuframesDeviceLost",
"CuframesShmError",
"CuframesProtocolMismatch",
"CuframesInvalidArgument",
"CuframesOutOfMemory",
"CuframesInternal",
]
+47
View File
@@ -0,0 +1,47 @@
[build-system]
requires = [
"scikit-build-core>=0.10",
"pybind11>=2.13",
]
build-backend = "scikit_build_core.build"
[project]
name = "cuframes"
version = "0.4.0"
description = "Python bindings for cuframes — zero-copy CUDA frame sharing"
readme = "README.md"
license = { text = "LGPL-2.1+" }
requires-python = ">=3.10"
authors = [{ name = "Evgeny Demchenko", email = "demchenkoev@gmail.com" }]
keywords = ["cuda", "video", "ipc", "zero-copy"]
classifiers = [
"Development Status :: 3 - Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU Lesser General Public License v2 or later (LGPLv2+)",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Multimedia :: Video",
]
[project.optional-dependencies]
torch = ["torch>=2.4"]
cupy = ["cupy-cuda12x>=13"]
dev = ["pytest>=8", "ruff>=0.6"]
[tool.scikit-build]
cmake.version = ">=3.20"
cmake.build-type = "Release"
build-dir = "build/{wheel_tag}"
wheel.packages = ["cuframes"]
# Будем строить только Python модуль; libcuframes собирается отдельно
# в основном CMake-проекте и линкуется как imported target.
cmake.args = ["-DBUILD_PYTHON_BINDINGS=ON", "-DBUILD_EXAMPLES=OFF", "-DBUILD_TOOLS=OFF"]
cmake.source-dir = ".."
[tool.scikit-build.cmake.define]
BUILD_PYTHON_BINDINGS = "ON"
[tool.pytest.ini_options]
testpaths = ["tests"]
+757
View File
@@ -0,0 +1,757 @@
// cuframes Python bindings — pybind11 entry point.
//
// Этот файл реализует core wrapper для subscriber-side API:
// - CuframesFrame — owning handle одного frame'а, context manager
// - CuframesSubscriber — owning handle subscription'а, context manager
//
// DLPack export (#199), per-subscriber CUDA stream (#201), health/stats props
// (#200) — добавляются в последующих коммитах в этот же файл.
//
// Контракт thread-safety (предварительный, финальный — task #202):
// - Каждый handle (CuframesSubscriber / CuframesFrame) принадлежит одному
// Python потоку. Cross-thread access = undefined behavior на C-уровне.
// - GIL отпускается на длинных I/O вызовах (next_frame) — другие Python
// потоки могут работать пока мы ждём frame.
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <cstring>
#include <optional>
#include <stdexcept>
#include <string>
#include "cuframes/cuframes.h"
// DLPack — стандартный protocol для exchange tensor-like структур между
// фреймворками (PyTorch/CuPy/JAX/TF). См. https://dmlc.github.io/dlpack/latest/
// Мы embedим header inline чтобы не добавлять external dep — header
// небольшой и стабильный (DLPack 1.0+).
namespace dlpack {
typedef enum {
kDLCPU = 1,
kDLCUDA = 2,
} DLDeviceType;
typedef struct {
DLDeviceType device_type;
int32_t device_id;
} DLDevice;
typedef enum {
kDLInt = 0U,
kDLUInt = 1U,
kDLFloat = 2U,
} DLDataTypeCode;
typedef struct {
uint8_t code;
uint8_t bits;
uint16_t lanes;
} DLDataType;
typedef struct {
void* data;
DLDevice device;
int32_t ndim;
DLDataType dtype;
int64_t* shape;
int64_t* strides;
uint64_t byte_offset;
} DLTensor;
typedef struct DLManagedTensor {
DLTensor dl_tensor;
void* manager_ctx;
void (*deleter)(struct DLManagedTensor* self);
} DLManagedTensor;
} // namespace dlpack
namespace py = pybind11;
namespace {
// ─────────────────────────────────────────────────────────────────────────────
// Error taxonomy — Python exceptions, соответствующие cuframes_error_t.
//
// Принцип: каждая категория ошибок которая требует разной обработки в
// downstream'е (reconnect vs retry vs fatal) → отдельный exception class.
// Это решает требование из architect review: «detector должен уметь
// reconnect-loop по publisher-gone, не падать».
// ─────────────────────────────────────────────────────────────────────────────
struct CuframesExceptions {
py::object base;
py::object publisher_gone; // CUFRAMES_ERR_DISCONNECTED, _NOT_FOUND
py::object frame_timeout; // CUFRAMES_ERR_TIMEOUT, _WOULD_BLOCK
py::object device_lost; // CUFRAMES_ERR_CUDA
py::object shm_error; // CUFRAMES_ERR_IO
py::object protocol_mismatch; // CUFRAMES_ERR_PROTOCOL
py::object invalid_argument; // CUFRAMES_ERR_INVALID_ARG
py::object out_of_memory; // CUFRAMES_ERR_OUT_OF_MEMORY
py::object internal; // CUFRAMES_ERR_INTERNAL, прочее
};
CuframesExceptions g_exc;
// Маппинг cuframes_error_t → подходящий Python exception class.
py::object exception_for(int err) {
switch (err) {
case CUFRAMES_ERR_NOT_FOUND:
case CUFRAMES_ERR_DISCONNECTED:
return g_exc.publisher_gone;
case CUFRAMES_ERR_TIMEOUT:
case CUFRAMES_ERR_WOULD_BLOCK:
return g_exc.frame_timeout;
case CUFRAMES_ERR_CUDA:
return g_exc.device_lost;
case CUFRAMES_ERR_IO:
return g_exc.shm_error;
case CUFRAMES_ERR_PROTOCOL:
return g_exc.protocol_mismatch;
case CUFRAMES_ERR_INVALID_ARG:
return g_exc.invalid_argument;
case CUFRAMES_ERR_OUT_OF_MEMORY:
return g_exc.out_of_memory;
default:
return g_exc.internal;
}
}
// Бросает подходящий exception если err != CUFRAMES_OK.
void check(int err, const char* operation = nullptr) {
if (err == CUFRAMES_OK) return;
const char* msg = cuframes_strerror(err);
std::string what = operation
? std::string(operation) + ": " + msg + " (code=" + std::to_string(err) + ")"
: std::string(msg) + " (code=" + std::to_string(err) + ")";
PyErr_SetString(exception_for(err).ptr(), what.c_str());
throw py::error_already_set();
}
// ─────────────────────────────────────────────────────────────────────────────
// CuframesFrame — owning wrapper над cuframes_frame_t*.
//
// Lifecycle:
// - конструируется через Subscriber::next_frame() (single source of truth)
// - в destructor'е (или __exit__) автоматически вызывает release
// - после release() все property accessor'ы бросают CuframesError
// - non-copyable, non-movable из Python (PyObject identity)
//
// Frame держит **слабую** ссылку (raw pointer) на subscriber. Если subscriber
// уничтожен раньше frame'а — released() становится no-op (subscriber разрулит
// освобождение всех outstanding frames при cuframes_subscriber_destroy).
// Чтобы избежать use-after-free, frame проверяет sub_alive_ через shared_ptr.
//
// Для простоты Phase 0 — frame и subscriber должны жить в одном Python потоке,
// порядок destruction под управлением Python GC. Refcount на Python-стороне
// от субскриптора держится через py::object атрибут.
// ─────────────────────────────────────────────────────────────────────────────
class FrameWrapper {
public:
FrameWrapper(cuframes_subscriber_t* sub, cuframes_frame_t* frame)
: sub_(sub), frame_(frame) {}
~FrameWrapper() {
try { release(); } catch (...) { /* destructor — глотаем */ }
}
// pybind11 не любит copyable wrappers для owning ресурсов.
FrameWrapper(const FrameWrapper&) = delete;
FrameWrapper& operator=(const FrameWrapper&) = delete;
bool released() const noexcept { return frame_ == nullptr; }
void release() {
if (frame_ != nullptr) {
// sub_ может быть nullptr если subscriber разорвал связь раньше —
// в этом случае release уже не нужен (subscriber всё освободил).
if (sub_ != nullptr) {
cuframes_subscriber_release(sub_, frame_);
}
frame_ = nullptr;
}
}
// Internal hook — subscriber говорит frame'у «я умираю, не release()ай».
void invalidate_subscriber() noexcept { sub_ = nullptr; }
// ── Properties ──────────────────────────────────────────────────────
// Все геттеры проверяют released() — иначе CuframesError.
void check_alive() const {
if (frame_ == nullptr) {
PyErr_SetString(g_exc.base.ptr(), "frame has been released");
throw py::error_already_set();
}
}
uintptr_t cuda_ptr() const {
check_alive();
return reinterpret_cast<uintptr_t>(cuframes_frame_cuda_ptr(frame_));
}
cuframes_format_t format() const {
check_alive();
return cuframes_frame_format(frame_);
}
int width() const {
check_alive();
int32_t w, h;
cuframes_frame_size(frame_, &w, &h);
return w;
}
int height() const {
check_alive();
int32_t w, h;
cuframes_frame_size(frame_, &w, &h);
return h;
}
int pitch_y() const {
check_alive();
return cuframes_frame_pitch_y(frame_);
}
int pitch_uv() const {
check_alive();
return cuframes_frame_pitch_uv(frame_);
}
uint64_t seq() const {
check_alive();
return cuframes_frame_seq(frame_);
}
int64_t pts_ns() const {
check_alive();
return cuframes_frame_pts_ns(frame_);
}
cuframes_subscriber_t* internal_sub() const noexcept { return sub_; }
cuframes_frame_t* internal_frame() const noexcept { return frame_; }
private:
cuframes_subscriber_t* sub_;
cuframes_frame_t* frame_;
};
// ─────────────────────────────────────────────────────────────────────────────
// DLPack export helpers.
//
// Кадр в NV12 состоит из 2 plane'ов: Y (uint8, H×W, pitch=pitch_y) и
// UV interleaved (uint8, H/2×W, pitch=pitch_uv; W здесь = ширина в байтах
// для interleaved U+V).
//
// Стратегия: даём пользователю 2 отдельных DLPack capsule на каждый plane.
// Это стандартный pattern в PyTorch/CuPy (torchcodec, cuda-python).
// UV offset вычисляется из pitch_y * height_aligned (NVDEC выравнивает
// height до aligned значения — обычно высота уже aligned, но мы используем
// видимую height из frame_size).
//
// Lifetime: deleter capsule освобождает только shape/strides arrays.
// Сам CUDA pointer принадлежит frame'у — gone-frame должно быть released
// **после** того как DLPack capsule destroyed. Чтобы не дать пользователю
// shoot in foot, capsule.manager_ctx держит py::object на FrameWrapper
// (увеличивает refcount), которое освобождается в deleter.
// ─────────────────────────────────────────────────────────────────────────────
struct DLPackContext {
py::object frame_keep_alive; // CuframesFrame Python-side
std::vector<int64_t> shape;
std::vector<int64_t> strides;
};
static void dlpack_deleter(dlpack::DLManagedTensor* self) {
if (!self) return;
auto* ctx = static_cast<DLPackContext*>(self->manager_ctx);
if (ctx) {
// Releasing Python refcount требует GIL
py::gil_scoped_acquire gil;
delete ctx;
}
delete self;
}
static void dlpack_pycapsule_destructor(PyObject* capsule) {
if (PyCapsule_IsValid(capsule, "dltensor")) {
// Capsule НЕ был consumed downstream'ом (e.g. torch.from_dlpack).
// Нужно освободить managed tensor самим.
auto* mt = static_cast<dlpack::DLManagedTensor*>(
PyCapsule_GetPointer(capsule, "dltensor"));
if (mt && mt->deleter) {
mt->deleter(mt);
}
}
// Если PyCapsule имеет name "used_dltensor" — downstream взял ownership,
// мы ничего не делаем.
}
static py::capsule make_dlpack_capsule(
void* data,
int rows, int cols, int64_t row_stride_bytes,
int cuda_device,
py::object frame_keep_alive
) {
auto* ctx = new DLPackContext;
ctx->frame_keep_alive = std::move(frame_keep_alive);
ctx->shape = {static_cast<int64_t>(rows), static_cast<int64_t>(cols)};
ctx->strides = {row_stride_bytes, 1};
auto* mt = new dlpack::DLManagedTensor;
mt->dl_tensor.data = data;
mt->dl_tensor.device = {dlpack::kDLCUDA, cuda_device};
mt->dl_tensor.ndim = 2;
mt->dl_tensor.dtype = {dlpack::kDLUInt, 8, 1}; // uint8
mt->dl_tensor.shape = ctx->shape.data();
mt->dl_tensor.strides = ctx->strides.data();
mt->dl_tensor.byte_offset = 0;
mt->manager_ctx = ctx;
mt->deleter = dlpack_deleter;
return py::capsule(mt, "dltensor", &dlpack_pycapsule_destructor);
}
// ─────────────────────────────────────────────────────────────────────────────
// CuframesSubscriber — owning wrapper над cuframes_subscriber_t*.
//
// API:
// sub = cuframes.subscribe("cam-parking", consumer_name="yolo-world",
// timeout_ms=5000)
// with sub:
// with sub.next_frame(timeout_ms=1000) as frame:
// do_something(frame.cuda_ptr, frame.width, frame.height)
// # sub.close() здесь автоматически
//
// Iteration (Phase 0.5):
// for frame in sub.frames(timeout_ms=1000):
// ...
// ─────────────────────────────────────────────────────────────────────────────
// Per-subscriber health stats. Phase 0 версия — counted в pybind layer
// (cuframes C API не expose'ит ring_occupancy / drop_count напрямую).
// Если в будущем cuframes расширит C API (cuframes_subscriber_get_stats),
// добавим reads оттуда — но текущие counters остаются для совместимости
// с тем что consumer'у видно через Python API.
struct SubscriberStats {
uint64_t frames_received = 0; // успешных next_frame()
uint64_t timeouts = 0; // CUFRAMES_ERR_TIMEOUT / WOULD_BLOCK
uint64_t errors = 0; // прочие fail'ы в next_frame()
uint64_t last_seq = 0; // seq последнего полученного frame'а
uint64_t gap_count = 0; // сколько раз seq[i] > seq[i-1] + 1
// (proxy для drop count в NEWEST_ONLY mode)
int64_t last_frame_pts_ns = 0;
};
class SubscriberWrapper {
public:
SubscriberWrapper(
const std::string& key,
std::optional<std::string> consumer_name,
cuframes_subscriber_mode_t mode,
int cuda_device,
int connect_timeout_ms,
uintptr_t consumer_stream
) : key_(key),
consumer_name_(consumer_name.value_or("")),
mode_(mode),
cuda_device_(cuda_device),
consumer_stream_(reinterpret_cast<void*>(consumer_stream)) {
cuframes_subscriber_config_t cfg = {};
cfg.key = key_.c_str();
cfg.consumer_name = consumer_name.has_value() ? consumer_name_.c_str() : nullptr;
cfg.mode = mode_;
cfg.cuda_device = cuda_device_;
cfg.connect_timeout_ms = connect_timeout_ms;
// create — может быть блокирующим (ждёт publisher'а). GIL release.
int err;
{
py::gil_scoped_release rel;
err = cuframes_subscriber_create(&cfg, &sub_);
}
check(err, "cuframes_subscriber_create");
}
~SubscriberWrapper() {
try { close(); } catch (...) { /* destructor — глотаем */ }
}
SubscriberWrapper(const SubscriberWrapper&) = delete;
SubscriberWrapper& operator=(const SubscriberWrapper&) = delete;
bool closed() const noexcept { return sub_ == nullptr; }
void close() {
if (sub_ != nullptr) {
cuframes_subscriber_destroy(sub_);
sub_ = nullptr;
}
}
void check_alive() const {
if (sub_ == nullptr) {
PyErr_SetString(g_exc.base.ptr(), "subscriber has been closed");
throw py::error_already_set();
}
}
// Возвращает new FrameWrapper. Caller владеет через Python GC.
// GIL release на время блокирующего вызова — другие потоки работают.
std::unique_ptr<FrameWrapper> next_frame(int timeout_ms) {
check_alive();
cuframes_frame_t* raw = nullptr;
int err;
{
py::gil_scoped_release rel;
// Используем persistent per-subscriber stream — все consumer'ы
// получают независимый cudaStreamWaitEvent, не серializуются
// через default stream.
err = cuframes_subscriber_next(sub_, consumer_stream_,
&raw, timeout_ms);
}
// Update health stats до check() — иначе при exception они не
// увеличатся, и оператору будет непонятно почему counters застыли.
if (err == CUFRAMES_OK) {
stats_.frames_received++;
uint64_t seq = cuframes_frame_seq(raw);
if (stats_.last_seq != 0 && seq > stats_.last_seq + 1) {
stats_.gap_count++;
}
stats_.last_seq = seq;
stats_.last_frame_pts_ns = cuframes_frame_pts_ns(raw);
} else if (err == CUFRAMES_ERR_TIMEOUT || err == CUFRAMES_ERR_WOULD_BLOCK) {
stats_.timeouts++;
} else {
stats_.errors++;
}
check(err, "cuframes_subscriber_next");
return std::make_unique<FrameWrapper>(sub_, raw);
}
const std::string& key() const { return key_; }
const std::string& consumer_name() const { return consumer_name_; }
cuframes_subscriber_mode_t mode() const { return mode_; }
int cuda_device() const { return cuda_device_; }
const SubscriberStats& stats() const { return stats_; }
// Snapshot stats как Python dict — для MQTT health publish.
py::dict stats_dict() const {
py::dict d;
d["frames_received"] = stats_.frames_received;
d["timeouts"] = stats_.timeouts;
d["errors"] = stats_.errors;
d["last_seq"] = stats_.last_seq;
d["gap_count"] = stats_.gap_count;
d["last_frame_pts_ns"] = stats_.last_frame_pts_ns;
return d;
}
uintptr_t consumer_stream() const {
return reinterpret_cast<uintptr_t>(consumer_stream_);
}
private:
cuframes_subscriber_t* sub_ = nullptr;
std::string key_;
std::string consumer_name_;
cuframes_subscriber_mode_t mode_;
int cuda_device_;
// CUDA stream — opaque cudaStream_t. Передаётся снаружи как int
// (полученный через cuda-python / torch.cuda.Stream._as_parameter_).
// nullptr = default stream (только для smoke-тестов; в продакшене
// консумерам надо иметь свой stream чтобы избежать serialization
// через default).
void* consumer_stream_ = nullptr;
SubscriberStats stats_{};
};
} // namespace
PYBIND11_MODULE(_native, m) {
m.doc() = "cuframes — zero-copy CUDA frame sharing (native bindings)";
// ── Версия ──────────────────────────────────────────────────────────
m.def("version_string", []() {
return std::string(cuframes_version_string());
}, "Runtime version of libcuframes (MAJOR.MINOR.PATCH).");
m.def("protocol_version", []() {
return static_cast<uint32_t>(cuframes_protocol_version());
}, "Wire-protocol version. Subscribers с разной версией не подключатся.");
m.attr("__binding_version__") = CUFRAMES_PY_BINDING_VERSION;
// ── Error taxonomy ──────────────────────────────────────────────────
// Иерархия:
// CuframesError (base)
// ├── CuframesPublisherGone
// ├── CuframesFrameTimeout
// ├── CuframesDeviceLost
// ├── CuframesShmError
// ├── CuframesProtocolMismatch
// ├── CuframesInvalidArgument
// ├── CuframesOutOfMemory
// └── CuframesInternal
//
// py::exception<T>(...) уже возвращает py::object на сам Python class.
// Не вызываем .attr("__class__") — иначе получим metaclass.
g_exc.base = py::exception<std::runtime_error>(m, "CuframesError");
auto make_subexc = [&m](const char* name) -> py::object {
return py::exception<std::runtime_error>(m, name, g_exc.base.ptr());
};
g_exc.publisher_gone = make_subexc("CuframesPublisherGone");
g_exc.frame_timeout = make_subexc("CuframesFrameTimeout");
g_exc.device_lost = make_subexc("CuframesDeviceLost");
g_exc.shm_error = make_subexc("CuframesShmError");
g_exc.protocol_mismatch = make_subexc("CuframesProtocolMismatch");
g_exc.invalid_argument = make_subexc("CuframesInvalidArgument");
g_exc.out_of_memory = make_subexc("CuframesOutOfMemory");
g_exc.internal = make_subexc("CuframesInternal");
// ── Pixel formats (enum mirror) ─────────────────────────────────────
py::enum_<cuframes_format_t>(m, "PixelFormat")
.value("NV12", CUFRAMES_FORMAT_NV12)
.value("YUV420P", CUFRAMES_FORMAT_YUV420P)
.value("RGB", CUFRAMES_FORMAT_RGB)
.value("BGR", CUFRAMES_FORMAT_BGR)
.value("RGBA", CUFRAMES_FORMAT_RGBA)
.value("GRAYSCALE", CUFRAMES_FORMAT_GRAYSCALE);
py::enum_<cuframes_subscriber_mode_t>(m, "SubscriberMode")
.value("NEWEST_ONLY", CUFRAMES_MODE_NEWEST_ONLY)
.value("STRICT_ORDER", CUFRAMES_MODE_STRICT_ORDER);
// ── CuframesFrame ───────────────────────────────────────────────────
py::class_<FrameWrapper>(m, "CuframesFrame",
"Один кадр от cuframes publisher'а.\n\n"
"Получается через CuframesSubscriber.next_frame().\n"
"Поддерживает context manager — release() при выходе из with-блока.\n"
"Все property accessor'ы после release() бросают CuframesError.\n\n"
"Это handle на frame в ring buffer publisher'а — данные остаются\n"
"в shared memory publisher'а пока frame не released. Долго удерживать\n"
"frame нельзя: medленный consumer заставит publisher либо overwrite\n"
"(DROP_OLDEST policy), либо stall (STRICT_WAIT).")
// properties (read-only)
.def_property_readonly("cuda_ptr", &FrameWrapper::cuda_ptr,
"CUDA device pointer на frame data (uintptr_t). Read-only для\n"
"consumer'а. Используйте через cuda-python / cupy / torch.from_blob.")
.def_property_readonly("format", &FrameWrapper::format,
"PixelFormat (NV12 для NVDEC publisher'а).")
.def_property_readonly("width", &FrameWrapper::width)
.def_property_readonly("height", &FrameWrapper::height)
.def_property_readonly("pitch_y", &FrameWrapper::pitch_y,
"Pitch (байт на строку) для Y plane. ВАЖНО: для больших\n"
"разрешений (2688×1520, gate_lpr) pitch != width — kernel'ы\n"
"должны принимать pitch как параметр.")
.def_property_readonly("pitch_uv", &FrameWrapper::pitch_uv,
"Pitch для UV plane (NV12/YUV420P); 0 для форматов без UV.")
.def_property_readonly("seq", &FrameWrapper::seq,
"Sequence number — монотонная нумерация у publisher'а.")
.def_property_readonly("pts_ns", &FrameWrapper::pts_ns,
"Presentation timestamp от publisher'а (наносекунды, CLOCK_MONOTONIC).")
.def_property_readonly("released", &FrameWrapper::released)
.def("release", &FrameWrapper::release,
"Освободить frame обратно publisher'у (ACK).\n"
"После release() property accessor'ы бросают CuframesError.\n"
"Idempotent — повторный вызов no-op.")
// context manager
.def("__enter__", [](FrameWrapper& self) -> FrameWrapper& {
self.check_alive();
return self;
}, py::return_value_policy::reference_internal)
.def("__exit__", [](FrameWrapper& self, py::object, py::object, py::object) {
self.release();
return py::none();
})
.def("__repr__", [](const FrameWrapper& f) {
if (f.released()) return std::string("<CuframesFrame released>");
return std::string("<CuframesFrame seq=") + std::to_string(f.seq()) +
" size=" + std::to_string(f.width()) + "x" + std::to_string(f.height()) + ">";
})
// ── DLPack export ───────────────────────────────────────────────
// Multi-plane formats (NV12, YUV420P) — экспортируем планы отдельно
// как 2D uint8 tensors. Consumer строит логику склейки сам.
// Для single-plane (RGB/BGR/RGBA/GRAYSCALE) — __dlpack__() работает.
.def("dlpack_y",
[](py::object self) -> py::capsule {
auto& f = self.cast<FrameWrapper&>();
f.check_alive();
void* ptr = cuframes_frame_cuda_ptr(f.internal_frame());
int32_t w, h;
cuframes_frame_size(f.internal_frame(), &w, &h);
int pitch = cuframes_frame_pitch_y(f.internal_frame());
// Для NV12/YUV420P width = ширина в пикселях, Y занимает W байт/строка.
// Pitch (физическая строка в памяти) может быть > W. Передаём как stride.
// cuda_device извлекаем не из frame (нет API) — фиксируем 0 для default;
// task #201 добавит per-subscriber stream и реальный device.
return make_dlpack_capsule(ptr, h, w, pitch, /*cuda_device=*/0, self);
},
"DLPack export Y-plane как 2D uint8 GPU tensor (shape=[H, W], stride=[pitch_y, 1]).\n"
"Работает для NV12, YUV420P, GRAYSCALE. Для других форматов — отдаёт первый plane.")
.def("dlpack_uv",
[](py::object self) -> py::capsule {
auto& f = self.cast<FrameWrapper&>();
f.check_alive();
auto fmt = cuframes_frame_format(f.internal_frame());
if (fmt != CUFRAMES_FORMAT_NV12) {
PyErr_SetString(g_exc.invalid_argument.ptr(),
"dlpack_uv() only supported for NV12 format");
throw py::error_already_set();
}
void* base = cuframes_frame_cuda_ptr(f.internal_frame());
int32_t w, h;
cuframes_frame_size(f.internal_frame(), &w, &h);
int pitch_y = cuframes_frame_pitch_y(f.internal_frame());
int pitch_uv = cuframes_frame_pitch_uv(f.internal_frame());
// NV12 layout: Y plane занимает pitch_y * h bytes,
// UV plane (interleaved U+V) следует сразу за ним.
void* uv_ptr = static_cast<uint8_t*>(base) + (size_t)pitch_y * h;
// UV plane размеры: H/2 строк, W колонок (interleaved U+V байты).
return make_dlpack_capsule(uv_ptr, h / 2, w, pitch_uv, /*cuda_device=*/0, self);
},
"DLPack export UV-plane (interleaved) для NV12.\n"
"Shape=[H/2, W] uint8, stride=[pitch_uv, 1]. U и V interleaved\n"
"по байтам в последнем измерении (W = ширина в пикселях, но\n"
"каждый pixel = 2 байта U+V).")
.def("__dlpack__",
[](py::object self, py::object /*stream*/) -> py::capsule {
// PEP 3118 / DLPack protocol — single-plane access.
// Для NV12/YUV420P возвращает Y plane (это самый частый use
// case — motion detection / brightness работают только с Y).
// Если нужен UV — явно через .dlpack_uv().
auto& f = self.cast<FrameWrapper&>();
f.check_alive();
void* ptr = cuframes_frame_cuda_ptr(f.internal_frame());
int32_t w, h;
cuframes_frame_size(f.internal_frame(), &w, &h);
int pitch = cuframes_frame_pitch_y(f.internal_frame());
return make_dlpack_capsule(ptr, h, w, pitch, /*cuda_device=*/0, self);
},
py::arg("stream") = py::none(),
"DLPack protocol для torch.from_dlpack / cupy.from_dlpack.\n"
"Для NV12 возвращает Y plane. Для других planes — .dlpack_uv().")
.def("__dlpack_device__",
[](const FrameWrapper& f) -> py::tuple {
f.check_alive();
// (device_type, device_id) — kDLCUDA=2, device 0 (task #201).
return py::make_tuple(2, 0);
},
"DLPack device protocol — возвращает (kDLCUDA=2, device_id).");
// ── CuframesSubscriber ──────────────────────────────────────────────
py::class_<SubscriberWrapper>(m, "CuframesSubscriber",
"Subscription на cuframes publisher.\n\n"
"Создаётся через cuframes.subscribe(key, ...). Поддерживает context\n"
"manager — close() при выходе из with-блока.\n\n"
"Thread-safety contract:\n"
" • Handle принадлежит одному Python потоку — создание и\n"
" все вызовы (next_frame, close) должны быть в одном thread.\n"
" • Несколько subscriber'ов в разных потоках — OK (каждому свой\n"
" handle, свой CUDA stream).\n"
" • Доступ к Frame после release() из другого потока — UB\n"
" (cuframes_frame_t* указывает в ring buffer publisher'а, после\n"
" release он может быть переписан).\n"
" • Внутренний GIL отпускается на длинных I/O вызовах\n"
" (subscriber_create, next_frame) — другие Python потоки могут\n"
" выполняться параллельно пока мы ждём frame.\n\n"
"CUDA stream:\n"
" consumer_stream передаётся как int (cudaStream_t как opaque\n"
" pointer). Получается через cuda-python (cudart.cudaStreamCreate)\n"
" или torch (torch.cuda.Stream()._as_parameter_). Если 0 —\n"
" default stream (serialization risk при нескольких subscriber'ах\n"
" в одном процессе).")
.def(py::init<const std::string&, std::optional<std::string>,
cuframes_subscriber_mode_t, int, int, uintptr_t>(),
py::arg("key"),
py::arg("consumer_name") = py::none(),
py::arg("mode") = CUFRAMES_MODE_NEWEST_ONLY,
py::arg("cuda_device") = 0,
py::arg("connect_timeout_ms") = -1,
py::arg("consumer_stream") = 0,
"Создать subscription. Блокирует до publisher_ready или\n"
"connect_timeout_ms. -1 = ждать вечно, 0 = fail сразу.\n"
"consumer_stream: int representation cudaStream_t (0=default).")
.def_property_readonly("key", &SubscriberWrapper::key)
.def_property_readonly("consumer_name", &SubscriberWrapper::consumer_name)
.def_property_readonly("mode", &SubscriberWrapper::mode)
.def_property_readonly("cuda_device", &SubscriberWrapper::cuda_device)
.def_property_readonly("consumer_stream", &SubscriberWrapper::consumer_stream,
"Pointer на cudaStream_t (int). 0 = default stream.")
.def_property_readonly("closed", &SubscriberWrapper::closed)
.def("next_frame", &SubscriberWrapper::next_frame,
py::arg("timeout_ms") = -1,
"Получить следующий frame.\n\n"
"timeout_ms: -1 = ждать вечно; 0 = non-blocking\n"
"(CuframesFrameTimeout если нет данных); >0 = с таймаутом.\n\n"
"Возвращает CuframesFrame — context manager. Использовать через\n"
"`with sub.next_frame() as frame: ...` для гарантии release.")
.def("close", &SubscriberWrapper::close,
"Закрыть subscription. Idempotent.")
// ── Health / stats ──────────────────────────────────────────────
// Phase 0: counted в pybind layer (cuframes C API не expose'ит
// ring_occupancy / drop_count напрямую). Эти counters достаточно
// для MQTT health publisher / monitoring.
.def_property_readonly("frames_received",
[](const SubscriberWrapper& s) { return s.stats().frames_received; },
"Количество успешных next_frame() с момента subscribe.")
.def_property_readonly("timeouts",
[](const SubscriberWrapper& s) { return s.stats().timeouts; },
"Сколько раз next_frame() вернул CuframesFrameTimeout.")
.def_property_readonly("errors",
[](const SubscriberWrapper& s) { return s.stats().errors; },
"Сколько раз next_frame() упал с error (не timeout).")
.def_property_readonly("last_seq",
[](const SubscriberWrapper& s) { return s.stats().last_seq; },
"Sequence number последнего полученного frame'а.")
.def_property_readonly("gap_count",
[](const SubscriberWrapper& s) { return s.stats().gap_count; },
"Сколько раз seq[i] > seq[i-1] + 1 — proxy для drop count\n"
"в NEWEST_ONLY mode. В STRICT_ORDER должен оставаться 0.")
.def_property_readonly("last_frame_pts_ns",
[](const SubscriberWrapper& s) { return s.stats().last_frame_pts_ns; })
.def("stats",
[](const SubscriberWrapper& s) { return s.stats_dict(); },
"Snapshot всех health counters как dict — для MQTT health publish.")
// context manager
.def("__enter__", [](SubscriberWrapper& self) -> SubscriberWrapper& {
self.check_alive();
return self;
}, py::return_value_policy::reference_internal)
.def("__exit__", [](SubscriberWrapper& self, py::object, py::object, py::object) {
self.close();
return py::none();
})
.def("__repr__", [](const SubscriberWrapper& s) {
return std::string("<CuframesSubscriber key='") + s.key() +
"' closed=" + (s.closed() ? "True" : "False") + ">";
});
// ── Module-level factory ────────────────────────────────────────────
// Удобный shortcut: cuframes.subscribe("cam-parking") вместо
// cuframes._native.CuframesSubscriber(...).
m.def("subscribe",
[](const std::string& key,
std::optional<std::string> consumer_name,
cuframes_subscriber_mode_t mode,
int cuda_device,
int connect_timeout_ms,
uintptr_t consumer_stream) {
return std::make_unique<SubscriberWrapper>(
key, consumer_name, mode, cuda_device,
connect_timeout_ms, consumer_stream);
},
py::arg("key"),
py::arg("consumer_name") = py::none(),
py::arg("mode") = CUFRAMES_MODE_NEWEST_ONLY,
py::arg("cuda_device") = 0,
py::arg("connect_timeout_ms") = -1,
py::arg("consumer_stream") = 0,
"Создать CuframesSubscriber. Shortcut для CuframesSubscriber(...).");
}
+112
View File
@@ -0,0 +1,112 @@
"""Smoke tests для cuframes Python bindings.
В Phase 0 (skeleton) проверяем что:
- модуль импортируется
- версия читается
- error классы существуют и являются нормальной иерархией
Subscriber / DLPack тесты появятся в следующих фазах
(см. issue gx/cuframes#6, tasks #198+).
"""
import cuframes
def test_version_format():
v = cuframes.version_string()
assert isinstance(v, str)
parts = v.split(".")
assert len(parts) >= 3
assert all(p.isdigit() for p in parts[:3])
def test_protocol_version_is_uint():
pv = cuframes.protocol_version()
assert isinstance(pv, int)
assert pv >= 0
def test_pixel_format_enum_members():
assert cuframes.PixelFormat.NV12.value == 0
assert cuframes.PixelFormat.YUV420P.value == 1
def test_subscriber_mode_enum_members():
assert cuframes.SubscriberMode.NEWEST_ONLY.value == 0
assert cuframes.SubscriberMode.STRICT_ORDER.value == 1
def test_error_hierarchy():
"""Все subtype'ы наследуются от CuframesError."""
for sub in [
cuframes.CuframesPublisherGone,
cuframes.CuframesFrameTimeout,
cuframes.CuframesDeviceLost,
cuframes.CuframesShmError,
cuframes.CuframesProtocolMismatch,
cuframes.CuframesInvalidArgument,
cuframes.CuframesOutOfMemory,
cuframes.CuframesInternal,
]:
assert issubclass(sub, cuframes.CuframesError)
def test_subscriber_class_exposed():
"""CuframesSubscriber/CuframesFrame exposed как public classes."""
assert hasattr(cuframes, "CuframesSubscriber")
assert hasattr(cuframes, "CuframesFrame")
assert hasattr(cuframes, "subscribe")
def test_subscribe_to_missing_publisher_raises():
"""Subscribe к несуществующему publisher → CuframesError (subclass)
после connect_timeout_ms.
Этот тест работает на любом хосте (без живого cuframes-pub) — мы
верифицируем что error path работает и маппит CUFRAMES_ERR_*
в правильный Python exception.
"""
import pytest
with pytest.raises(cuframes.CuframesError):
cuframes.subscribe(
"definitely-not-existing-publisher-xyz",
connect_timeout_ms=100,
)
def test_subscriber_repr_when_unable_to_connect():
"""Лёгкий тест что repr не падает и close idempotent."""
import pytest
try:
sub = cuframes.subscribe("nope-xyz", connect_timeout_ms=100)
except cuframes.CuframesError:
return # ожидаемо
pytest.fail("subscribe должно было выкинуть exception")
def test_subscribe_accepts_consumer_stream_param():
"""consumer_stream — uintptr (cudaStream_t).
Проверяем что параметр accepted; реальное использование требует
cuda-python / torch.cuda.Stream — это в integration тестах
yolo-world-detector'а.
"""
import pytest
with pytest.raises(cuframes.CuframesError):
cuframes.subscribe(
"nope-xyz",
connect_timeout_ms=100,
consumer_stream=0, # 0 = default stream
)
def test_subscribe_kwargs_signature():
"""Проверяем что у subscribe правильный набор kwargs."""
import inspect
# Pybind11-обёртки не дают inspect.signature, но help_doc отражает их.
doc = cuframes.subscribe.__doc__
assert "consumer_name" in doc
assert "mode" in doc
assert "cuda_device" in doc
assert "connect_timeout_ms" in doc
assert "consumer_stream" in doc
+4
View File
@@ -0,0 +1,4 @@
vmm_fd_pingpong/producer
vmm_fd_pingpong/consumer
smoke_v04/smoke_pub
smoke_v04/smoke_sub
+13
View File
@@ -0,0 +1,13 @@
CFLAGS = -O2 -Wall -I../../include -I/usr/local/cuda/include
LDFLAGS = -L../../build-v04/libcuframes -lcuframes -L/usr/local/cuda/lib64 -lcudart -lcuda -lpthread -lrt
all: smoke_pub smoke_sub
smoke_pub: smoke_pub.c
gcc $(CFLAGS) -o $@ $< $(LDFLAGS)
smoke_sub: smoke_sub.c
gcc $(CFLAGS) -o $@ $< $(LDFLAGS)
clean:
rm -f smoke_pub smoke_sub
+55
View File
@@ -0,0 +1,55 @@
/* v0.4 smoke test publisher — NV12 1920x1080 ring 4, fill каждый slot
* с pattern (i % 256), publish, infinite loop. */
#include <cuframes/cuframes.h>
#include <cuda_runtime.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
int main(int argc, char **argv) {
const char *key = argc > 1 ? argv[1] : "smoke";
cuframes_publisher_config_t cfg = {0};
cfg.key = key;
cfg.width = 1920;
cfg.height = 1080;
cfg.format = CUFRAMES_FORMAT_NV12;
cfg.ownership = CUFRAMES_OWNERSHIP_LIBRARY;
cfg.ring_size = 4;
cfg.policy = CUFRAMES_POLICY_DROP_OLDEST;
cfg.cuda_device = 0;
cuframes_publisher_t *pub = NULL;
int r = cuframes_publisher_create(&cfg, &pub);
if (r != CUFRAMES_OK) {
fprintf(stderr, "publisher create failed: %d (%s)\n", r, cuframes_strerror(r));
return 1;
}
fprintf(stderr, "publisher 'cuframes-%s' ready (v0.4 VMM)\n", key);
cudaStream_t stream;
cudaStreamCreate(&stream);
int i = 0;
while (1) {
void *ptr = NULL;
r = cuframes_publisher_acquire(pub, &ptr);
if (r != CUFRAMES_OK) { fprintf(stderr, "acquire: %d\n", r); break; }
uint8_t pattern = (uint8_t)(i & 0xFF);
cudaMemsetAsync(ptr, pattern, 1920 * 1080 * 3 / 2, stream);
r = cuframes_publisher_publish(pub, stream,
(int64_t)cuframes_now_ns());
if (r != CUFRAMES_OK) { fprintf(stderr, "publish: %d\n", r); break; }
i++;
if (i % 50 == 0) fprintf(stderr, "published %d frames\n", i);
struct timespec ts = {.tv_sec = 0, .tv_nsec = 40000000}; /* 25 fps */
nanosleep(&ts, NULL);
}
cudaStreamDestroy(stream);
cuframes_publisher_destroy(pub);
return 0;
}
+63
View File
@@ -0,0 +1,63 @@
/* v0.4 smoke subscriber — connect, read 100 frames, verify pattern, exit 0/1. */
#include <cuframes/cuframes.h>
#include <cuda_runtime.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char **argv) {
const char *key = argc > 1 ? argv[1] : "smoke";
cuframes_subscriber_config_t cfg = {0};
cfg.key = key;
cfg.consumer_name = "smoke-sub";
cfg.mode = CUFRAMES_MODE_NEWEST_ONLY;
cfg.cuda_device = 0;
cfg.connect_timeout_ms = 10000;
cuframes_subscriber_t *sub = NULL;
int r = cuframes_subscriber_create(&cfg, &sub);
if (r != CUFRAMES_OK) {
fprintf(stderr, "subscriber create failed: %d (%s)\n", r, cuframes_strerror(r));
return 1;
}
fprintf(stderr, "subscribed to '%s' (v0.4)\n", key);
cudaStream_t stream;
cudaStreamCreate(&stream);
size_t check_size = 1024; /* sample 1KB чтобы не тратить время */
uint8_t *host = malloc(check_size);
int frames = 0;
int good = 0;
while (frames < 100) {
cuframes_frame_t *f = NULL;
r = cuframes_subscriber_next(sub, stream, &f, 2000);
if (r != CUFRAMES_OK) {
fprintf(stderr, "next failed: %d (%s)\n", r, cuframes_strerror(r));
break;
}
cudaMemcpyAsync(host, cuframes_frame_cuda_ptr(f), check_size,
cudaMemcpyDeviceToHost, stream);
cudaStreamSynchronize(stream);
uint8_t exp = host[0];
int mismatch = 0;
for (size_t i = 1; i < check_size; i++) {
if (host[i] != exp) { mismatch++; }
}
if (mismatch == 0) good++;
if (frames % 20 == 0) {
fprintf(stderr, "frame seq=%lu byte0=0x%02x mismatch=%d\n",
(unsigned long)cuframes_frame_seq(f), exp, mismatch);
}
cuframes_subscriber_release(sub, f);
frames++;
}
free(host);
cudaStreamDestroy(stream);
cuframes_subscriber_destroy(sub);
fprintf(stderr, "DONE: %d/%d frames OK\n", good, frames);
return (good == frames && frames > 0) ? 0 : 1;
}
+16
View File
@@ -0,0 +1,16 @@
CC = gcc
CFLAGS = -O2 -Wall -I/usr/local/cuda/include
LDFLAGS = -L/usr/local/cuda/lib64 -lcuda
all: producer consumer
producer: producer.c common.h
$(CC) $(CFLAGS) -o $@ producer.c $(LDFLAGS)
consumer: consumer.c common.h
$(CC) $(CFLAGS) -o $@ consumer.c $(LDFLAGS)
clean:
rm -f producer consumer
.PHONY: all clean
+69
View File
@@ -0,0 +1,69 @@
# vmm_fd_pingpong — spike для cuframes v0.4
Проверка: можно ли заменить CUDA IPC mem handles на VMM (cuMemCreate)
+ POSIX FD export, чтобы убрать требование shared pid/ipc namespaces
между producer и consumer контейнерами.
## Результат: ✅ работает
Запуск 2 контейнеров без shared pid/ipc, только volume mount для
unix-сокета:
```
producer: granularity=2097152
producer: alloc size=16777216
producer: exported fd=37 for handle
producer: listening on /run/spike/pingpong.sock, awaiting consumer...
consumer: connected to producer
consumer: recv fd=38 size=16777216 magic=0xa7
consumer: imported handle OK
consumer: mapped + access OK
consumer: verify mismatch=0/1048576 → ACK=O
consumer: done (OK)
```
## Ключевые наблюдения
- **Granularity на 5090 = 2 MB**. 1920×1080 NV12 (~3.1 MB) округлится до 4 MB.
16 slots × 4 камеры × +1 MB = +64 MB VRAM поверх текущих cuda IPC аллокаций.
- **FD передаётся через `sendmsg(SCM_RIGHTS)`** — kernel прокидывает реальный FD
в receiver namespace, переименовывая в свободный номер. Volume mount unix
socket'а — единственное требование (`/run/cuframes` уже монтируется как shared).
- **`cuMemImportFromShareableHandle`** принимает FD как `(void *)(uintptr_t)fd`.
- **Доступ на consumer side требует `cuMemSetAccess` с правильным `CUmemLocation`** —
device id из своего `cuDeviceGet`, не наследуется от producer.
## Замена events (упрощение этапа C)
CUDA events для IPC не имеют POSIX FD path. Внедрять external semaphores
(OPAQUE_FD) — отдельный API, другая sigal/wait семантика. **Вместо этого:**
producer вызывает `cuStreamSynchronize(stream)` ПЕРЕД `atomic_store(seq)` в
`do_publish`. Consumer тогда просто читает seq и копирует DtoD — без event wait.
Overhead: ~1 ms на publish × 25 fps = 2.5% CPU time producer'а. Memory
coherence гарантирована (один GPU, hardware ensures writes visible после
stream sync).
## Сборка
```bash
docker run --rm -v $PWD:/work -w /work nvidia/cuda:12.4.1-devel-ubuntu22.04 \
bash -c "apt-get install -y build-essential && make"
```
## Запуск теста
```bash
sudo mkdir -p /var/run/spike-pingpong && sudo chmod 777 /var/run/spike-pingpong
docker run -d --name spike-prod --runtime=nvidia --gpus all \
-v $PWD:/work -v /var/run/spike-pingpong:/run/spike \
nvidia/cuda:12.4.1-base-ubuntu22.04 /work/producer
docker run --rm --name spike-cons --runtime=nvidia --gpus all \
-v $PWD:/work -v /var/run/spike-pingpong:/run/spike \
nvidia/cuda:12.4.1-base-ubuntu22.04 /work/consumer
docker logs spike-prod && docker rm -f spike-prod
```
+20
View File
@@ -0,0 +1,20 @@
#pragma once
#include <cuda.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#define POOL_SIZE (16 * 1024 * 1024)
#define MAGIC_BYTE 0xA7
#define SOCK_PATH "/run/spike/pingpong.sock"
#define CHECK(expr) do { \
CUresult _r = (expr); \
if (_r != CUDA_SUCCESS) { \
const char *_msg = NULL; \
cuGetErrorString(_r, &_msg); \
fprintf(stderr, "%s:%d %s -> %d (%s)\n", \
__FILE__, __LINE__, #expr, (int)_r, _msg ? _msg : "?"); \
exit(1); \
} \
} while (0)
+97
View File
@@ -0,0 +1,97 @@
#include "common.h"
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
static int recv_fd(int sock, int *out_fd, uint64_t *out_size, uint8_t *out_magic) {
struct msghdr msg = {0};
char ctrl[CMSG_SPACE(sizeof(int))];
struct iovec iov[2];
iov[0].iov_base = out_size; iov[0].iov_len = sizeof(*out_size);
iov[1].iov_base = out_magic; iov[1].iov_len = sizeof(*out_magic);
msg.msg_iov = iov; msg.msg_iovlen = 2;
msg.msg_control = ctrl; msg.msg_controllen = sizeof(ctrl);
ssize_t n = recvmsg(sock, &msg, 0);
if (n < 0) { perror("recvmsg"); return -1; }
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
if (!cmsg || cmsg->cmsg_level != SOL_SOCKET || cmsg->cmsg_type != SCM_RIGHTS) {
fprintf(stderr, "no SCM_RIGHTS in msg\n");
return -1;
}
memcpy(out_fd, CMSG_DATA(cmsg), sizeof(int));
return 0;
}
int main(void) {
CHECK(cuInit(0));
CUdevice dev;
CHECK(cuDeviceGet(&dev, 0));
CUcontext ctx;
CHECK(cuCtxCreate(&ctx, 0, dev));
/* Connect to producer */
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock < 0) { perror("socket"); return 1; }
struct sockaddr_un sa = {.sun_family = AF_UNIX};
strncpy(sa.sun_path, SOCK_PATH, sizeof(sa.sun_path) - 1);
for (int retry = 0; retry < 50; retry++) {
if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) == 0) break;
if (retry == 49) { perror("connect (final)"); return 1; }
usleep(100000);
}
fprintf(stderr, "consumer: connected to producer\n");
int fd = -1;
uint64_t size = 0;
uint8_t magic = 0;
if (recv_fd(sock, &fd, &size, &magic) < 0) return 1;
fprintf(stderr, "consumer: recv fd=%d size=%llu magic=0x%02x\n",
fd, (unsigned long long)size, magic);
CUmemGenericAllocationHandle mem;
CHECK(cuMemImportFromShareableHandle(&mem, (void *)(uintptr_t)fd,
CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR));
fprintf(stderr, "consumer: imported handle OK\n");
CUdeviceptr ptr;
CHECK(cuMemAddressReserve(&ptr, size, 0, 0, 0));
CHECK(cuMemMap(ptr, size, 0, mem, 0));
CUmemAccessDesc access = {0};
access.location.type = CU_MEM_LOCATION_TYPE_DEVICE;
access.location.id = dev;
access.flags = CU_MEM_ACCESS_FLAGS_PROT_READ;
CHECK(cuMemSetAccess(ptr, size, &access, 1));
fprintf(stderr, "consumer: mapped + access OK\n");
/* Copy out 1MB чтобы убедиться что pattern там */
size_t check = size < (1 << 20) ? size : (1 << 20);
uint8_t *host = malloc(check);
CHECK(cuMemcpyDtoH(host, ptr, check));
CHECK(cuCtxSynchronize());
size_t mismatch = 0;
for (size_t i = 0; i < check; i++) {
if (host[i] != magic) mismatch++;
}
free(host);
char ack = (mismatch == 0) ? 'O' : 'X';
fprintf(stderr, "consumer: verify mismatch=%zu/%zu → ACK=%c\n",
mismatch, check, ack);
write(sock, &ack, 1);
close(sock);
close(fd);
CHECK(cuMemUnmap(ptr, size));
CHECK(cuMemAddressFree(ptr, size));
CHECK(cuMemRelease(mem));
CHECK(cuCtxDestroy(ctx));
fprintf(stderr, "consumer: done (%s)\n", ack == 'O' ? "OK" : "FAIL");
return ack == 'O' ? 0 : 1;
}
+103
View File
@@ -0,0 +1,103 @@
#include "common.h"
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <unistd.h>
/* Send fd через SCM_RIGHTS вместе с (uint64_t size, uint8_t magic) payload. */
static int send_fd(int sock, int fd, uint64_t size, uint8_t magic) {
struct msghdr msg = {0};
char ctrl[CMSG_SPACE(sizeof(int))];
struct iovec iov[2];
iov[0].iov_base = &size; iov[0].iov_len = sizeof(size);
iov[1].iov_base = &magic; iov[1].iov_len = sizeof(magic);
msg.msg_iov = iov; msg.msg_iovlen = 2;
msg.msg_control = ctrl; msg.msg_controllen = sizeof(ctrl);
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
memcpy(CMSG_DATA(cmsg), &fd, sizeof(int));
ssize_t n = sendmsg(sock, &msg, 0);
if (n < 0) { perror("sendmsg"); return -1; }
return 0;
}
int main(void) {
CHECK(cuInit(0));
CUdevice dev;
CHECK(cuDeviceGet(&dev, 0));
CUcontext ctx;
CHECK(cuCtxCreate(&ctx, 0, dev));
CUmemAllocationProp prop = {0};
prop.type = CU_MEM_ALLOCATION_TYPE_PINNED;
prop.location.type = CU_MEM_LOCATION_TYPE_DEVICE;
prop.location.id = dev;
prop.requestedHandleTypes = CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR;
size_t granularity = 0;
CHECK(cuMemGetAllocationGranularity(&granularity, &prop,
CU_MEM_ALLOC_GRANULARITY_MINIMUM));
fprintf(stderr, "producer: granularity=%zu\n", granularity);
size_t size = ((POOL_SIZE + granularity - 1) / granularity) * granularity;
fprintf(stderr, "producer: alloc size=%zu\n", size);
CUmemGenericAllocationHandle mem;
CHECK(cuMemCreate(&mem, size, &prop, 0));
CUdeviceptr ptr;
CHECK(cuMemAddressReserve(&ptr, size, 0, 0, 0));
CHECK(cuMemMap(ptr, size, 0, mem, 0));
CUmemAccessDesc access = {0};
access.location = prop.location;
access.flags = CU_MEM_ACCESS_FLAGS_PROT_READWRITE;
CHECK(cuMemSetAccess(ptr, size, &access, 1));
/* Fill with MAGIC pattern */
CHECK(cuMemsetD8(ptr, MAGIC_BYTE, size));
CHECK(cuCtxSynchronize());
int fd;
CHECK(cuMemExportToShareableHandle(&fd, mem,
CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR, 0));
fprintf(stderr, "producer: exported fd=%d for handle\n", fd);
/* Unix socket server */
unlink(SOCK_PATH);
int srv = socket(AF_UNIX, SOCK_STREAM, 0);
if (srv < 0) { perror("socket"); return 1; }
struct sockaddr_un sa = {.sun_family = AF_UNIX};
strncpy(sa.sun_path, SOCK_PATH, sizeof(sa.sun_path) - 1);
if (bind(srv, (struct sockaddr *)&sa, sizeof(sa)) < 0) { perror("bind"); return 1; }
if (listen(srv, 1) < 0) { perror("listen"); return 1; }
fprintf(stderr, "producer: listening on %s, awaiting consumer...\n", SOCK_PATH);
int cli = accept(srv, NULL, NULL);
if (cli < 0) { perror("accept"); return 1; }
if (send_fd(cli, fd, (uint64_t)size, MAGIC_BYTE) < 0) return 1;
fprintf(stderr, "producer: sent fd + size=%zu + magic=0x%02x\n",
size, MAGIC_BYTE);
/* Wait for consumer ACK */
char ack;
if (read(cli, &ack, 1) != 1) { perror("read ack"); return 1; }
fprintf(stderr, "producer: got ACK=0x%02x\n", (unsigned char)ack);
close(cli);
close(srv);
unlink(SOCK_PATH);
close(fd);
CHECK(cuMemUnmap(ptr, size));
CHECK(cuMemAddressFree(ptr, size));
CHECK(cuMemRelease(mem));
CHECK(cuCtxDestroy(ctx));
fprintf(stderr, "producer: done\n");
return ack == 'O' ? 0 : 1;
}
+94 -22
View File
@@ -60,6 +60,9 @@ struct Args {
bool verbose = false;
bool realtime = false; // emulate -re у ffmpeg CLI: sleep по pts
bool loop = false; // loop input на eof (для file://)
bool enable_packet_ring = false; // v0.2 — публиковать encoded packets
std::string policy = "drop"; // "drop" = DROP_OLDEST, "wait" = STRICT_WAIT
int ack_timeout_ms = 200; // only used при policy=wait; <=0 = infinite (unsafe)
};
static void print_usage() {
@@ -75,6 +78,16 @@ static void print_usage() {
" --ring N cuframes ring size (default 4, range 2..16)\n"
" --realtime pace input по PTS (как ffmpeg -re; полезно для файла)\n"
" --loop loop input на EOF (только для file://)\n"
" --enable-packet-ring v0.2: дополнительно публиковать encoded packets\n"
" (для consumer'ов с -c:v copy, Frigate record path)\n"
" --policy MODE drop (default) = DROP_OLDEST — producer wrap'ает ring\n"
" без ожидания consumer ack. Подходит для multi-consumer.\n"
" wait = STRICT_WAIT — producer ждёт ack от всех subscribers\n"
" перед overwrite. Безопаснее для frame integrity, но slow\n"
" consumer задерживает all (default ack-timeout 200ms).\n"
" --ack-timeout-ms N только при --policy wait. Max wait для ack (default 200).\n"
" <=0 = infinite — НЕ РЕКОМЕНДУЕТСЯ (dead consumer вешает\n"
" producer навсегда).\n"
" --verbose debug logs\n"
" -h, --help this help\n";
}
@@ -92,11 +105,24 @@ static int parse_args(int argc, char **argv, Args &a) {
else if (s == "--ring") a.ring_size = std::stoi(next());
else if (s == "--realtime") a.realtime = true;
else if (s == "--loop") a.loop = true;
else if (s == "--enable-packet-ring") a.enable_packet_ring = true;
else if (s == "--policy") a.policy = next();
else if (s == "--ack-timeout-ms") a.ack_timeout_ms = std::stoi(next());
else if (s == "--verbose") a.verbose = true;
else if (s == "-h" || s == "--help") { print_usage(); std::exit(0); }
else { std::cerr << "Unknown arg: " << s << "\n"; print_usage(); std::exit(1); }
}
if (a.rtsp_url.empty() || a.key.empty()) { print_usage(); return 1; }
if (a.policy != "drop" && a.policy != "wait") {
std::cerr << "Invalid --policy '" << a.policy << "' (use drop|wait)\n";
return 1;
}
if (a.policy == "wait" && a.ack_timeout_ms <= 0) {
std::cerr << "WARNING: --policy wait + --ack-timeout-ms<=0 = infinite wait.\n"
<< " Dead consumer повесит producer навсегда. Forcing к 200ms.\n"
<< " Set явно --ack-timeout-ms 200 (или больше) чтобы убрать warning.\n";
a.ack_timeout_ms = 200;
}
return 0;
}
@@ -205,35 +231,54 @@ int main(int argc, char **argv) {
return 2;
}
/* Pre-allocate cuframes pool (NV12 — что nvdec выдаёт) */
/* Pre-allocate cuframes pool (NV12 — что nvdec выдаёт).
* v0.4: publisher сам аллоцирует через cuMemCreate (VMM). Раньше tool
* передавал external pool, но v0.4 не может export'нуть cudaMalloc-pointers
* как POSIX FD — VMM API требует cuMemCreate-allocated memory. */
int32_t pitch_y = 0, pitch_uv = 0;
size_t frame_size = cuframes::calc_frame_size(CUFRAMES_FORMAT_NV12,
width, height,
&pitch_y, &pitch_uv);
cudaSetDevice(a.cuda_device);
std::vector<void *> pool(a.ring_size, nullptr);
for (int i = 0; i < a.ring_size; ++i) {
cudaError_t cerr = cudaMalloc(&pool[i], frame_size);
if (cerr != cudaSuccess) {
std::cerr << "cudaMalloc pool[" << i << "]: " << cudaGetErrorString(cerr) << "\n";
return 2;
}
}
cuframes::PublisherOptions po;
po.key = a.key;
po.width = width;
po.height = height;
po.format = CUFRAMES_FORMAT_NV12;
po.policy = CUFRAMES_POLICY_DROP_OLDEST;
po.policy = (a.policy == "wait")
? CUFRAMES_POLICY_STRICT_WAIT
: CUFRAMES_POLICY_DROP_OLDEST;
po.consumer_ack_timeout_ms = a.ack_timeout_ms;
po.cuda_device = a.cuda_device;
po.ring_size = a.ring_size; /* для logging */
po.ring_size = a.ring_size;
cuframes::Publisher pub(po, pool.data(), a.ring_size, frame_size);
cuframes::Publisher pub(po); /* LIBRARY ownership — publisher owns VMM pool */
std::cerr << "[cuframes-src] publisher 'cuframes-" << a.key
<< "' ready, ring=" << a.ring_size
<< " pool_size=" << frame_size << " bytes/frame\n";
<< "' ready (v0.4 VMM), ring=" << a.ring_size
<< " frame_size=" << frame_size << " bytes\n";
/* v0.2 — encoded packet ring (опционально). */
if (a.enable_packet_ring) {
cuframes_packet_ring_options_t pkt_opts{};
pkt_opts.codec_id = (uint32_t)vstream->codecpar->codec_id;
/* остальные поля = 0 → library использует defaults (64 slots, 8MiB, 2MiB max) */
pub.enable_packets(&pkt_opts);
if (vstream->codecpar->extradata_size > 0 && vstream->codecpar->extradata) {
pub.set_codec_extradata(vstream->codecpar->extradata,
(size_t)vstream->codecpar->extradata_size);
std::cerr << "[cuframes-src] packet ring active, codec_id="
<< vstream->codecpar->codec_id
<< " extradata=" << vstream->codecpar->extradata_size
<< " bytes\n";
} else {
std::cerr << "[cuframes-src] packet ring active, codec_id="
<< vstream->codecpar->codec_id
<< " (no extradata in stream — will rely on in-band SPS/PPS)\n";
}
}
/* Stream для D2D copies */
cudaStream_t stream;
@@ -243,7 +288,6 @@ int main(int argc, char **argv) {
AVFrame *frame = av_frame_alloc();
if (!pkt || !frame) return 2;
int pool_idx = 0;
uint64_t frame_count = 0;
auto t_last_log = std::chrono::steady_clock::now();
uint64_t last_log_count = 0;
@@ -279,6 +323,29 @@ int main(int argc, char **argv) {
continue;
}
/* v0.2 — публикуем encoded packet в packet ring ДО decoder. Это позволяет
* record-consumer'ам брать packet без второго RTSP-подключения к камере. */
if (a.enable_packet_ring) {
int64_t pkt_pts_ns = (pkt->pts != AV_NOPTS_VALUE)
? av_rescale_q(pkt->pts, stream_tb, AVRational{1, 1000000000})
: cuframes::now_ns();
int64_t pkt_dts_ns = (pkt->dts != AV_NOPTS_VALUE)
? av_rescale_q(pkt->dts, stream_tb, AVRational{1, 1000000000})
: pkt_pts_ns;
uint32_t pkt_flags = 0;
if (pkt->flags & AV_PKT_FLAG_KEY) pkt_flags |= CUFRAMES_PKT_FLAG_KEY;
if (pkt->flags & AV_PKT_FLAG_CORRUPT) pkt_flags |= CUFRAMES_PKT_FLAG_CORRUPT;
#ifdef AV_PKT_FLAG_DISCONTINUITY
if (pkt->flags & AV_PKT_FLAG_DISCONTINUITY) pkt_flags |= CUFRAMES_PKT_FLAG_DISCONTINUITY;
#endif
int prr = pub.publish_packet(pkt->data, (size_t)pkt->size,
pkt_pts_ns, pkt_dts_ns, pkt_flags);
if (prr != CUFRAMES_OK && a.verbose) {
std::cerr << "[cuframes-src] publish_packet rc=" << prr
<< " size=" << pkt->size << "\n";
}
}
r = avcodec_send_packet(ctx, pkt);
av_packet_unref(pkt);
if (r < 0) continue;
@@ -302,7 +369,15 @@ int main(int argc, char **argv) {
int src_pitch_y = frame->linesize[0];
int src_pitch_uv = frame->linesize[1];
void *dst = pool[pool_idx];
/* v0.4: acquire slot из publisher's VMM pool */
void *dst = nullptr;
try {
dst = pub.acquire();
} catch (const cuframes::Error &e) {
std::cerr << "acquire: " << e.what() << "\n";
av_frame_unref(frame);
continue;
}
/* D2D 2D-copy Y plane */
cudaError_t cerr = cudaMemcpy2DAsync(
@@ -340,14 +415,13 @@ int main(int argc, char **argv) {
int64_t pts_ns = cuframes::now_ns();
try {
pub.publish_external(dst, stream, pts_ns);
pub.publish(stream, pts_ns);
} catch (const cuframes::Error &e) {
std::cerr << "publish_external: " << e.what() << "\n";
std::cerr << "publish: " << e.what() << "\n";
av_frame_unref(frame);
continue;
}
pool_idx = (pool_idx + 1) % a.ring_size;
frame_count++;
av_frame_unref(frame);
@@ -374,9 +448,7 @@ int main(int argc, char **argv) {
av_buffer_unref(&hw_device);
cudaStreamDestroy(stream);
/* Publisher destructor freed first; теперь освободим pool */
/* Note: publisher уже destroyed by RAII, IPC handles closed by subscribers */
for (auto p : pool) if (p) cudaFree(p);
/* v0.4: publisher owns VMM pool — destructor освободит cuMemRelease etc. */
return 0;
}