docs: full content + landing + RU translations
Initial documentation site for cuframes:
- Landing page (src/pages/index.mdx) — hero, quick example (publisher +
subscriber), comparison table vs naive/DeepStream, honest "early but
production-tested" status
- /docs/intro — full overview
- /docs/getting-started/{install,first-publisher,first-subscriber}
- /docs/concepts/{frame-vs-packet-ring,ownership-modes,sync-vmm-stream}
with mermaid diagrams
- /docs/integration/{ffmpeg-demuxer,ffmpeg-filter,python}
- /docs/reference/{api-c,api-cpp,protocol} — full v4 wire protocol spec
incl. VMM_FDS message, magic 0xCC7C1DCE bump diff
- /docs/faq — comparison vs DeepStream/GStreamer, license, multi-host
limitations
- i18n/ru/ — parallel RU translation (tech terms latin, склонение апостроф)
Build:
- Docusaurus 3.10.1 + theme-mermaid + search-local
- Follows dagstack-* docs convention (canonical: dagstack-plugin-system-docs)
- Apache-2.0 license; cuframes lib itself remains LGPL-2.1+
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -75,7 +75,7 @@ int main(int argc, char **argv) {
|
|||||||
|
|
||||||
**`cuframes_publisher_acquire(pub, &ptr)`** — returns a CUDA device pointer to the next writable slot. Valid only until the matching `publish()` call.
|
**`cuframes_publisher_acquire(pub, &ptr)`** — returns a CUDA device pointer to the next writable slot. Valid only until the matching `publish()` call.
|
||||||
|
|
||||||
**`cudaMemsetAsync(ptr, ..., stream)`** — fill the frame on a CUDA stream of your choice. You do **not** have to synchronize this stream before publishing. The library will issue `cudaEventRecord` on the same stream inside `publish()`, and each subscriber will `cudaStreamWaitEvent` on its own stream before reading. This is the cross-process synchronization contract — see [Concepts → Cross-process sync](/docs/concepts/cross-process-sync).
|
**`cudaMemsetAsync(ptr, ..., stream)`** — fill the frame on a CUDA stream of your choice. You do **not** have to synchronize before calling `publish()`. The library issues `cuStreamSynchronize(stream)` inside `publish()` to flush pending GPU writes, then atomically publishes the sequence number. Subscribers see the data via hardware coherence on a same-GPU DtoD copy — no CUDA events needed. Full rationale: [Concepts → Sync: stream sync, not CUDA events](/docs/concepts/sync-vmm-stream).
|
||||||
|
|
||||||
**`cuframes_publisher_publish(pub, stream, pts_ns)`** — make the slot visible to subscribers. The `pts_ns` is opaque to the library; the recommended source is `cuframes_now_ns()` (CLOCK_MONOTONIC in nanoseconds).
|
**`cuframes_publisher_publish(pub, stream, pts_ns)`** — make the slot visible to subscribers. The `pts_ns` is opaque to the library; the recommended source is `cuframes_now_ns()` (CLOCK_MONOTONIC in nanoseconds).
|
||||||
|
|
||||||
|
|||||||
@@ -142,4 +142,4 @@ if (r == CUFRAMES_ERR_DISCONNECTED) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
A reconnect pattern, including back-off and `consumer_name` reuse caveats, is covered in [Concepts → Reconnect](/docs/concepts/reconnect).
|
A reconnect pattern, including back-off and `consumer_name` reuse caveats, is : detect `CUFRAMES_ERR_DISCONNECTED`, call `cuframes_subscriber_destroy()`, back off (1-2 sec), and `cuframes_subscriber_create()` again with the same key. The FFmpeg `cuframes://` demuxer does this automatically (see [Integration → FFmpeg demuxer](/docs/integration/ffmpeg-demuxer)).
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ sidebar_position: 1
|
|||||||
|
|
||||||
cuframes is **Linux only**. The IPC mechanism relies on POSIX shared memory and `SCM_RIGHTS` file-descriptor passing over Unix sockets. Windows, macOS and WSL2 are not supported.
|
cuframes is **Linux only**. The IPC mechanism relies on POSIX shared memory and `SCM_RIGHTS` file-descriptor passing over Unix sockets. Windows, macOS and WSL2 are not supported.
|
||||||
|
|
||||||
You also need an NVIDIA GPU with compute capability ≥ 7.5 (Turing or newer) and a CUDA 12+ driver. See [Concepts → Requirements](/docs/concepts/requirements) for the full matrix.
|
You also need an NVIDIA GPU with compute capability ≥ 7.5 (Turing or newer) and a CUDA 12+ driver. Specifically: 64-bit Linux, glibc 2.31+, kernel 5.4+ (for `cuMemMap` + `SCM_RIGHTS` support).
|
||||||
|
|
||||||
## Option 1 — Pre-built Docker image (recommended for trying it out)
|
## Option 1 — Pre-built Docker image (recommended for trying it out)
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ docker run --rm --runtime=nvidia \
|
|||||||
/usr/local/bin/sub_count --key cam1 --max-frames 100
|
/usr/local/bin/sub_count --key cam1 --max-frames 100
|
||||||
```
|
```
|
||||||
|
|
||||||
See [Concepts → Docker IPC](/docs/concepts/docker-ipc) for the underlying namespace rules.
|
Namespace rules: subscriber must share IPC namespace with the publisher (POSIX `/dev/shm` lives in IPC ns). PID sharing is **not** required (this is a v0.4 property — pre-v0.4 needed both).
|
||||||
|
|
||||||
## Option 2 — Build from source
|
## Option 2 — Build from source
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -75,7 +75,7 @@ int main(int argc, char **argv) {
|
|||||||
|
|
||||||
**`cuframes_publisher_acquire(pub, &ptr)`** — возвращает CUDA device pointer на следующий writable slot. Действителен только до соответствующего вызова `publish()`.
|
**`cuframes_publisher_acquire(pub, &ptr)`** — возвращает CUDA device pointer на следующий writable slot. Действителен только до соответствующего вызова `publish()`.
|
||||||
|
|
||||||
**`cudaMemsetAsync(ptr, ..., stream)`** — заполняем frame на CUDA stream'е по вашему выбору. **Не нужно** синхронизировать этот stream перед publish. Library внутри `publish()` сделает `cudaEventRecord` на тот же stream, а каждый subscriber вызовет `cudaStreamWaitEvent` на своём stream'е перед чтением. Это и есть cross-process контракт синхронизации — см. [Концепции → Cross-process sync](/docs/concepts/cross-process-sync).
|
**`cudaMemsetAsync(ptr, ..., stream)`** — заполняем frame на CUDA stream'е по вашему выбору. **Не нужно** синхронизировать stream до вызова `publish()`. Library внутри `publish()` делает `cuStreamSynchronize(stream)` чтобы дождаться flush pending GPU writes, потом атомарно публикует sequence number. Subscriber видит данные через hardware coherence при DtoD memcpy на том же GPU — никакие CUDA events не нужны. Полное обоснование: [Концепции → Sync: stream sync, не CUDA events](/docs/concepts/sync-vmm-stream).
|
||||||
|
|
||||||
**`cuframes_publisher_publish(pub, stream, pts_ns)`** — делает slot видимым subscriber'ам. `pts_ns` непрозрачен для library; рекомендуемый источник — `cuframes_now_ns()` (CLOCK_MONOTONIC в наносекундах).
|
**`cuframes_publisher_publish(pub, stream, pts_ns)`** — делает slot видимым subscriber'ам. `pts_ns` непрозрачен для library; рекомендуемый источник — `cuframes_now_ns()` (CLOCK_MONOTONIC в наносекундах).
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -142,4 +142,4 @@ if (r == CUFRAMES_ERR_DISCONNECTED) {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Паттерн reconnect, включая back-off и нюансы переиспользования `consumer_name`, разобран в [Концепции → Reconnect](/docs/concepts/reconnect).
|
Паттерн reconnect, включая back-off и нюансы переиспользования `consumer_name`, : поймать `CUFRAMES_ERR_DISCONNECTED`, вызвать `cuframes_subscriber_destroy()`, подождать (1-2 сек) и попробовать `cuframes_subscriber_create()` снова с тем же key. FFmpeg-демухер `cuframes://` делает это автоматически (см. [Интеграция → FFmpeg demuxer](/docs/integration/ffmpeg-demuxer)).
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ sidebar_position: 1
|
|||||||
|
|
||||||
cuframes работает **только на Linux**. IPC-механизм опирается на POSIX shared memory и передачу file descriptors через `SCM_RIGHTS` поверх Unix sockets. Windows, macOS и WSL2 не поддерживаются.
|
cuframes работает **только на Linux**. IPC-механизм опирается на POSIX shared memory и передачу file descriptors через `SCM_RIGHTS` поверх Unix sockets. Windows, macOS и WSL2 не поддерживаются.
|
||||||
|
|
||||||
Также нужен NVIDIA GPU с compute capability ≥ 7.5 (Turing или новее) и CUDA 12+ driver. Полная матрица — в [Концепции → Требования](/docs/concepts/requirements).
|
Также нужен NVIDIA GPU с compute capability ≥ 7.5 (Turing или новее) и CUDA 12+ driver. Точнее: 64-bit Linux, glibc 2.31+, ядро 5.4+ (для `cuMemMap` + `SCM_RIGHTS`).
|
||||||
|
|
||||||
## Вариант 1 — готовый Docker-образ (рекомендуется для первого знакомства)
|
## Вариант 1 — готовый Docker-образ (рекомендуется для первого знакомства)
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ docker run --rm --runtime=nvidia \
|
|||||||
/usr/local/bin/sub_count --key cam1 --max-frames 100
|
/usr/local/bin/sub_count --key cam1 --max-frames 100
|
||||||
```
|
```
|
||||||
|
|
||||||
Подробнее о правилах namespace — [Концепции → Docker IPC](/docs/concepts/docker-ipc).
|
Правила namespace: subscriber должен share IPC namespace с publisher (POSIX `/dev/shm` живёт в IPC ns). PID sharing **не** нужен (это свойство v0.4 — до v0.4 нужны были оба).
|
||||||
|
|
||||||
## Вариант 2 — сборка из исходников
|
## Вариант 2 — сборка из исходников
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,531 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 1
|
||||||
|
title: C API
|
||||||
|
---
|
||||||
|
|
||||||
|
# C API reference
|
||||||
|
|
||||||
|
Полный листинг public C API из `<cuframes/cuframes.h>` (libcuframes 0.4.0). Source of truth — header в repo, эта страница его дублирует в Docusaurus-формате с cross-links на концептуальные разделы.
|
||||||
|
|
||||||
|
## Headers & linkage
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <cuframes/cuframes.h>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# pkg-config (если установлено через .deb)
|
||||||
|
cc app.c $(pkg-config --cflags --libs cuframes)
|
||||||
|
|
||||||
|
# вручную
|
||||||
|
cc app.c -lcuframes
|
||||||
|
```
|
||||||
|
|
||||||
|
`libcuframes.so.0` динамически линкуется к `libcuda.so.1` (CUDA driver API, не runtime). Для большинства user-кода также нужен `-lcudart`, чтобы манипулировать своими CUDA streams.
|
||||||
|
|
||||||
|
## Соглашения
|
||||||
|
|
||||||
|
- Все функции возвращают `int` — `0` (CUFRAMES_OK) при успехе, отрицательный код из [`cuframes_error_t`](#error-codes) при ошибке. Расшифровка кода — [`cuframes_strerror`](#error-decoding).
|
||||||
|
- Все handle types (`cuframes_publisher_t`, `cuframes_subscriber_t`, `cuframes_frame_t`, `cuframes_packet_t`) — **opaque**. Поля недоступны напрямую, только через accessor-функции. Это даёт ABI-stability в minor-релизах.
|
||||||
|
- Каждый handle принадлежит **одному потоку**. Cross-thread access — undefined behavior. Несколько handle'ов в разных потоках — OK.
|
||||||
|
- Endianness — little-endian (это и так фиксируется CUDA-платформами).
|
||||||
|
|
||||||
|
## Версии и error codes
|
||||||
|
|
||||||
|
### Версия библиотеки
|
||||||
|
|
||||||
|
```c
|
||||||
|
const char *cuframes_version_string(void);
|
||||||
|
uint32_t cuframes_protocol_version(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
`cuframes_version_string` возвращает runtime-версию libcuframes в формате `"MAJOR.MINOR.PATCH"` (например `"0.4.0"`). Compile-time константы:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define CUFRAMES_VERSION_MAJOR 0
|
||||||
|
#define CUFRAMES_VERSION_MINOR 4
|
||||||
|
#define CUFRAMES_VERSION_PATCH 0
|
||||||
|
```
|
||||||
|
|
||||||
|
`cuframes_protocol_version` возвращает версию wire-protocol (для v0.4 — `4`). Subscribers с другой protocol-версией не подключатся — publisher вернёт `HELLO_RESP(result=CUFRAMES_ERR_PROTOCOL)`. См. [Protocol reference](/docs/reference/protocol).
|
||||||
|
|
||||||
|
### Error codes
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum cuframes_error {
|
||||||
|
CUFRAMES_OK = 0,
|
||||||
|
CUFRAMES_ERR_INVALID_ARG = -1,
|
||||||
|
CUFRAMES_ERR_OUT_OF_MEMORY = -2,
|
||||||
|
CUFRAMES_ERR_CUDA = -3,
|
||||||
|
CUFRAMES_ERR_IO = -4,
|
||||||
|
CUFRAMES_ERR_NOT_FOUND = -5,
|
||||||
|
CUFRAMES_ERR_ALREADY_EXISTS = -6,
|
||||||
|
CUFRAMES_ERR_TIMEOUT = -7,
|
||||||
|
CUFRAMES_ERR_PROTOCOL = -8,
|
||||||
|
CUFRAMES_ERR_DISCONNECTED = -9,
|
||||||
|
CUFRAMES_ERR_FORMAT = -10,
|
||||||
|
CUFRAMES_ERR_WOULD_BLOCK = -11,
|
||||||
|
CUFRAMES_ERR_TOO_MANY = -12,
|
||||||
|
CUFRAMES_ERR_PACKET_OVERSIZED = -20,
|
||||||
|
CUFRAMES_ERR_NO_PACKET_RING = -21,
|
||||||
|
CUFRAMES_ERR_NO_CODEC_PARAMS = -22,
|
||||||
|
CUFRAMES_ERR_PACKET_OVERRUN = -23,
|
||||||
|
CUFRAMES_ERR_INTERNAL = -100,
|
||||||
|
} cuframes_error_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
| Код | Имя | Значение |
|
||||||
|
|---|---|---|
|
||||||
|
| `0` | `CUFRAMES_OK` | Success |
|
||||||
|
| `-1` | `CUFRAMES_ERR_INVALID_ARG` | NULL pointer или невалидное значение в config |
|
||||||
|
| `-2` | `CUFRAMES_ERR_OUT_OF_MEMORY` | malloc / cudaMalloc fail |
|
||||||
|
| `-3` | `CUFRAMES_ERR_CUDA` | Ошибка CUDA runtime / driver |
|
||||||
|
| `-4` | `CUFRAMES_ERR_IO` | socket / mmap / eventfd |
|
||||||
|
| `-5` | `CUFRAMES_ERR_NOT_FOUND` | Publisher с таким key не найден |
|
||||||
|
| `-6` | `CUFRAMES_ERR_ALREADY_EXISTS` | Publisher с этим key уже есть, либо `consumer_name` занят |
|
||||||
|
| `-7` | `CUFRAMES_ERR_TIMEOUT` | Операция не завершилась за timeout |
|
||||||
|
| `-8` | `CUFRAMES_ERR_PROTOCOL` | Несовместимая версия wire protocol |
|
||||||
|
| `-9` | `CUFRAMES_ERR_DISCONNECTED` | Publisher умер или сеть оборвалась |
|
||||||
|
| `-10` | `CUFRAMES_ERR_FORMAT` | Неподдерживаемый pixel format или несовпадение размеров |
|
||||||
|
| `-11` | `CUFRAMES_ERR_WOULD_BLOCK` | Non-blocking call — данных пока нет |
|
||||||
|
| `-12` | `CUFRAMES_ERR_TOO_MANY` | Превышен `MAX_SUBSCRIBERS` (32) |
|
||||||
|
| `-20` | `CUFRAMES_ERR_PACKET_OVERSIZED` | `publish_packet` size > `max_packet_size` |
|
||||||
|
| `-21` | `CUFRAMES_ERR_NO_PACKET_RING` | Subscriber запросил packets, у publisher'а нет ring'а |
|
||||||
|
| `-22` | `CUFRAMES_ERR_NO_CODEC_PARAMS` | Extradata ещё не set publisher'ом |
|
||||||
|
| `-23` | `CUFRAMES_ERR_PACKET_OVERRUN` | Slow subscriber, packet seq уехал — resync на keyframe |
|
||||||
|
| `-100` | `CUFRAMES_ERR_INTERNAL` | Bug в библиотеке — воспроизводи и репорти |
|
||||||
|
|
||||||
|
### Расшифровка ошибок
|
||||||
|
|
||||||
|
```c
|
||||||
|
const char *cuframes_strerror(int err);
|
||||||
|
```
|
||||||
|
|
||||||
|
Возвращает human-readable строку для error code. Pointer указывает на static storage, владеть им дальше не надо. Никогда не возвращает NULL — для unknown code вернёт `"unknown error"`.
|
||||||
|
|
||||||
|
## Pixel formats
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum cuframes_format {
|
||||||
|
CUFRAMES_FORMAT_NV12 = 0,
|
||||||
|
CUFRAMES_FORMAT_YUV420P = 1,
|
||||||
|
CUFRAMES_FORMAT_RGB = 2,
|
||||||
|
CUFRAMES_FORMAT_BGR = 3,
|
||||||
|
CUFRAMES_FORMAT_RGBA = 4,
|
||||||
|
CUFRAMES_FORMAT_GRAYSCALE = 5,
|
||||||
|
} cuframes_format_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
| Format | Layout | Когда |
|
||||||
|
|---|---|---|
|
||||||
|
| `NV12` | Y plane + interleaved UV plane | NVDEC native, default для video pipeline'ов |
|
||||||
|
| `YUV420P` | Y + U + V separate planes | FFmpeg `yuv420p` |
|
||||||
|
| `RGB` | 24bpp packed RGB | ML inference, OpenGL |
|
||||||
|
| `BGR` | 24bpp packed BGR | OpenCV native |
|
||||||
|
| `RGBA` | 32bpp packed RGBA | overlays, compositing |
|
||||||
|
| `GRAYSCALE` | 8bpp single plane | depth maps, masks |
|
||||||
|
|
||||||
|
Format фиксирован для publisher'а в момент create — поменять нельзя без destroy + recreate с новым key.
|
||||||
|
|
||||||
|
## Policy & mode enums
|
||||||
|
|
||||||
|
### Publisher policy
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum cuframes_publisher_policy {
|
||||||
|
CUFRAMES_POLICY_DROP_OLDEST = 0,
|
||||||
|
CUFRAMES_POLICY_STRICT_WAIT = 1,
|
||||||
|
} cuframes_publisher_policy_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
- `DROP_OLDEST` — publisher не ждёт, перезаписывает next slot. Slow consumer пропускает кадры. **Default для real-time.**
|
||||||
|
- `STRICT_WAIT` — publisher блокируется, пока все подписанные consumers не ACK'нут. Не теряет кадры, но slowest consumer тормозит всех. Для recording или критичной аналитики.
|
||||||
|
|
||||||
|
### Subscriber mode
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum cuframes_subscriber_mode {
|
||||||
|
CUFRAMES_MODE_NEWEST_ONLY = 0,
|
||||||
|
CUFRAMES_MODE_STRICT_ORDER = 1,
|
||||||
|
} cuframes_subscriber_mode_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
- `NEWEST_ONLY` — брать самый свежий frame, пропускать промежуточные. **Default.**
|
||||||
|
- `STRICT_ORDER` — все frames по порядку. Если ring overflow — вернётся `CUFRAMES_ERR_DISCONNECTED`, нужно reconnect.
|
||||||
|
|
||||||
|
### Ownership mode
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef enum cuframes_ownership_mode {
|
||||||
|
CUFRAMES_OWNERSHIP_LIBRARY = 0,
|
||||||
|
CUFRAMES_OWNERSHIP_EXTERNAL = 1,
|
||||||
|
} cuframes_ownership_mode_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
- `LIBRARY` — library владеет VMM-pool'ом (см. [Sync model](/docs/concepts/sync-vmm-stream)). Publisher делает `acquire()` → пишет → `publish()`. **Единственный поддерживаемый mode в v0.4.**
|
||||||
|
- `EXTERNAL` — **в v0.4 deprecated.** `cuframes_publisher_create_external` возвращает `CUFRAMES_ERR_INVALID_ARG`. Для FFmpeg filter integration используй `LIBRARY` + одна D2D-копия в acquire'нутый slot (cuframes-rtsp-source именно так и работает с v0.4).
|
||||||
|
|
||||||
|
## Frame accessors
|
||||||
|
|
||||||
|
`cuframes_frame_t` — opaque handle на frame, полученный у subscriber'а. Валиден от `cuframes_subscriber_next` до `cuframes_subscriber_release`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct cuframes_frame cuframes_frame_t;
|
||||||
|
|
||||||
|
void *cuframes_frame_cuda_ptr(const cuframes_frame_t *frame);
|
||||||
|
cuframes_format_t cuframes_frame_format(const cuframes_frame_t *frame);
|
||||||
|
void cuframes_frame_size(const cuframes_frame_t *frame,
|
||||||
|
int32_t *width_out, int32_t *height_out);
|
||||||
|
int32_t cuframes_frame_pitch_y(const cuframes_frame_t *frame);
|
||||||
|
int32_t cuframes_frame_pitch_uv(const cuframes_frame_t *frame);
|
||||||
|
uint64_t cuframes_frame_seq(const cuframes_frame_t *frame);
|
||||||
|
int64_t cuframes_frame_pts_ns(const cuframes_frame_t *frame);
|
||||||
|
```
|
||||||
|
|
||||||
|
| Функция | Возвращает |
|
||||||
|
|---|---|
|
||||||
|
| `cuda_ptr` | CUDA device pointer на frame data (read-only для consumer'а) |
|
||||||
|
| `format` | `cuframes_format_t` |
|
||||||
|
| `size` | Ширина и высота в пикселях через out-параметры |
|
||||||
|
| `pitch_y` | Pitch (байт на строку) для Y plane или единственного plane |
|
||||||
|
| `pitch_uv` | Pitch для UV plane (NV12 / YUV420P); `0` для форматов без UV |
|
||||||
|
| `seq` | Sequence number — монотонная нумерация у publisher'а |
|
||||||
|
| `pts_ns` | Timestamp publisher'а (наносекунды, `CLOCK_MONOTONIC`) |
|
||||||
|
|
||||||
|
PTS epoch caveat: publisher и consumer могут иметь разные эпохи `CLOCK_MONOTONIC` (после publisher restart counter сбрасывается). Consumer должен sanity-checkить — например, детектить epoch change, когда `pts_ns_curr < pts_ns_prev`.
|
||||||
|
|
||||||
|
## Publisher API
|
||||||
|
|
||||||
|
### Config struct
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct cuframes_publisher_config {
|
||||||
|
const char *key; /* unique имя ("cam1"). Не NULL. */
|
||||||
|
int32_t width;
|
||||||
|
int32_t height;
|
||||||
|
cuframes_format_t format;
|
||||||
|
cuframes_ownership_mode_t ownership;
|
||||||
|
int32_t ring_size; /* 2..16, рекомендуется 4 */
|
||||||
|
cuframes_publisher_policy_t policy;
|
||||||
|
int32_t consumer_ack_timeout_ms; /* STRICT_WAIT; 0 = ждать вечно */
|
||||||
|
int32_t cuda_device;
|
||||||
|
uint64_t _reserved[4]; /* должно быть 0 */
|
||||||
|
} cuframes_publisher_config_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
| Поле | Ограничения |
|
||||||
|
|---|---|
|
||||||
|
| `key` | ASCII `[a-zA-Z0-9_-]{1,63}`. Не NULL. |
|
||||||
|
| `width`, `height` | Pixels. Фиксированы после create. |
|
||||||
|
| `format` | См. [Pixel formats](#pixel-formats). Фиксирован. |
|
||||||
|
| `ownership` | В v0.4 — только `LIBRARY`. |
|
||||||
|
| `ring_size` | 2..16 для `LIBRARY`. Меньше — больше шанс overrun, больше — больше VRAM. |
|
||||||
|
| `policy` | См. [Policy](#publisher-policy). |
|
||||||
|
| `consumer_ack_timeout_ms` | Только для `STRICT_WAIT`. `0` = ждать бесконечно. |
|
||||||
|
| `cuda_device` | Обычно `0`. Должен совпадать с consumer'ским. |
|
||||||
|
| `_reserved` | Reserved для ABI-stability, должно быть нулями. |
|
||||||
|
|
||||||
|
### Create / destroy
|
||||||
|
|
||||||
|
```c
|
||||||
|
int cuframes_publisher_create(const cuframes_publisher_config_t *cfg,
|
||||||
|
cuframes_publisher_t **out);
|
||||||
|
|
||||||
|
int cuframes_publisher_create_external(const cuframes_publisher_config_t *cfg,
|
||||||
|
void *const *cuda_ptrs,
|
||||||
|
int32_t ptr_count,
|
||||||
|
size_t frame_size,
|
||||||
|
cuframes_publisher_t **out);
|
||||||
|
|
||||||
|
int cuframes_publisher_destroy(cuframes_publisher_t *pub);
|
||||||
|
```
|
||||||
|
|
||||||
|
`cuframes_publisher_create` аллоцирует `ring_size` × `frame_size` через `cuMemCreate(POSIX_FILE_DESCRIPTOR)`, открывает Unix socket `/run/cuframes/<key>.sock`, mmap'ит `/dev/shm/cuframes-<key>`. См. [Synchronization & VMM stream](/docs/concepts/sync-vmm-stream).
|
||||||
|
|
||||||
|
Ошибки:
|
||||||
|
|
||||||
|
| Код | Когда |
|
||||||
|
|---|---|
|
||||||
|
| `INVALID_ARG` | `cfg` NULL, ring_size out of range, key не проходит regex |
|
||||||
|
| `ALREADY_EXISTS` | Publisher с этим key уже есть и его процесс живой |
|
||||||
|
| `CUDA` | `cuMemCreate` fail (out of VRAM, unsupported driver) |
|
||||||
|
| `IO` | Не получилось `bind()` socket или `shm_open()` |
|
||||||
|
|
||||||
|
`cuframes_publisher_create_external` — **в v0.4 возвращает `CUFRAMES_ERR_INVALID_ARG`**. EXTERNAL ownership убран потому, что VMM требует `cuMemCreate`-allocated memory. Для упомянутого FFmpeg filter use case — переходи на `LIBRARY` + одна `cudaMemcpyAsync(D2D)` в acquire'нутый slot. Cuframes-rtsp-source работает именно так начиная с v0.4.
|
||||||
|
|
||||||
|
`cuframes_publisher_destroy` шлёт `SHUTDOWN` всем connected subscribers, unlink'ает socket и shm. NULL-safe.
|
||||||
|
|
||||||
|
### Publish (LIBRARY mode)
|
||||||
|
|
||||||
|
```c
|
||||||
|
int cuframes_publisher_acquire(cuframes_publisher_t *pub,
|
||||||
|
void **cuda_ptr_out);
|
||||||
|
|
||||||
|
int cuframes_publisher_publish(cuframes_publisher_t *pub,
|
||||||
|
void *stream, /* cudaStream_t */
|
||||||
|
int64_t pts_ns);
|
||||||
|
```
|
||||||
|
|
||||||
|
`acquire` возвращает CUDA device pointer на следующий slot в ring'е для записи. Pointer стабилен, пока ты держишь ring slot — обычно до следующего `publish`.
|
||||||
|
|
||||||
|
Ошибки:
|
||||||
|
|
||||||
|
| Код | Когда |
|
||||||
|
|---|---|
|
||||||
|
| `TIMEOUT` | Все slots заняты в `STRICT_WAIT` mode |
|
||||||
|
| `INVALID_ARG` | `pub` NULL, или publisher был создан в EXTERNAL mode |
|
||||||
|
|
||||||
|
`publish` финализирует acquire'нутый slot. Внутри: `cuStreamSynchronize(stream)` гарантирует, что producer's writes hardware-coherent, затем atomic update `slot.seq` + `global_seq`. См. [Synchronization](/docs/concepts/sync-vmm-stream) — почему именно stream sync, а не CUDA events.
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
|---|---|
|
||||||
|
| `stream` | CUDA stream, на котором писались данные. `0` для default stream. |
|
||||||
|
| `pts_ns` | Timestamp, рекомендуется [`cuframes_now_ns()`](#utils). |
|
||||||
|
|
||||||
|
### Publish (EXTERNAL mode)
|
||||||
|
|
||||||
|
```c
|
||||||
|
int cuframes_publisher_publish_external(cuframes_publisher_t *pub,
|
||||||
|
void *cuda_ptr,
|
||||||
|
void *stream,
|
||||||
|
int64_t pts_ns);
|
||||||
|
```
|
||||||
|
|
||||||
|
**В v0.4 deprecated** — см. note про `create_external` выше. Всегда возвращает `CUFRAMES_ERR_INVALID_ARG`.
|
||||||
|
|
||||||
|
## Subscriber API (sync)
|
||||||
|
|
||||||
|
### Config struct
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct cuframes_subscriber_config {
|
||||||
|
const char *key;
|
||||||
|
const char *consumer_name; /* NULL = auto */
|
||||||
|
cuframes_subscriber_mode_t mode;
|
||||||
|
int32_t cuda_device;
|
||||||
|
int32_t connect_timeout_ms; /* 0=fail, -1=ждать вечно */
|
||||||
|
uint64_t _reserved[4];
|
||||||
|
} cuframes_subscriber_config_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
| Поле | Ограничения |
|
||||||
|
|---|---|
|
||||||
|
| `key` | Должен совпадать с publisher'ским |
|
||||||
|
| `consumer_name` | Если NULL — library сгенерирует `subscriber-<pid>-<random>`. Unique в пределах publisher'а — иначе `ALREADY_EXISTS`. MAX 32 subscribers. |
|
||||||
|
| `mode` | См. [Subscriber mode](#subscriber-mode) |
|
||||||
|
| `cuda_device` | Должен совпадать с publisher'ским — VMM FD импортируется на тот же device |
|
||||||
|
| `connect_timeout_ms` | `0` = fail сразу с `NOT_FOUND`; `-1` = ждать вечно |
|
||||||
|
|
||||||
|
### Create / destroy
|
||||||
|
|
||||||
|
```c
|
||||||
|
int cuframes_subscriber_create(const cuframes_subscriber_config_t *cfg,
|
||||||
|
cuframes_subscriber_t **out);
|
||||||
|
|
||||||
|
int cuframes_subscriber_destroy(cuframes_subscriber_t *sub);
|
||||||
|
```
|
||||||
|
|
||||||
|
`create` выполняет handshake (`HELLO` → `SUBSCRIBE` → `VMM_FDS`), импортирует N file descriptors через `cuMemImportFromShareableHandle`. См. [Protocol reference §3](/docs/reference/protocol).
|
||||||
|
|
||||||
|
Ошибки:
|
||||||
|
|
||||||
|
| Код | Когда |
|
||||||
|
|---|---|
|
||||||
|
| `NOT_FOUND` | Publisher с этим key не найден до `connect_timeout_ms` |
|
||||||
|
| `PROTOCOL` | Publisher имеет другую protocol version |
|
||||||
|
| `TOO_MANY` | Publisher уже имеет 32 subscriber'а |
|
||||||
|
| `ALREADY_EXISTS` | `consumer_name` занят |
|
||||||
|
| `CUDA` | `cuMemImportFromShareableHandle` fail |
|
||||||
|
|
||||||
|
`destroy` — graceful close: `UNSUBSCRIBE` msg → cleanup VMM mappings → close socket. NULL-safe.
|
||||||
|
|
||||||
|
### Next frame
|
||||||
|
|
||||||
|
```c
|
||||||
|
int cuframes_subscriber_next(cuframes_subscriber_t *sub,
|
||||||
|
void *consumer_stream,
|
||||||
|
cuframes_frame_t **frame_out,
|
||||||
|
int32_t timeout_ms);
|
||||||
|
|
||||||
|
int cuframes_subscriber_release(cuframes_subscriber_t *sub,
|
||||||
|
cuframes_frame_t *frame);
|
||||||
|
```
|
||||||
|
|
||||||
|
`next` блокируется до `timeout_ms`, ожидая новый frame. Семантика по mode:
|
||||||
|
|
||||||
|
- `NEWEST_ONLY` — возвращает самый свежий frame, пропускает промежуточные;
|
||||||
|
- `STRICT_ORDER` — следующий по seq; `DISCONNECTED` при overflow.
|
||||||
|
|
||||||
|
`consumer_stream` — твой CUDA stream, на котором будешь читать frame. В v0.4 синхронизация делается на стороне publisher'а через `cuStreamSynchronize`, так что параметр зарезервирован для будущего event-based fast path и сейчас не обязателен (`0` допустимо).
|
||||||
|
|
||||||
|
| Параметр | Значение |
|
||||||
|
|---|---|
|
||||||
|
| `consumer_stream` | CUDA stream consumer'а. `0` допустимо. |
|
||||||
|
| `frame_out` | Output handle. Освободить через `release`. |
|
||||||
|
| `timeout_ms` | `<0` = блокироваться, `0` = non-blocking (вернёт `WOULD_BLOCK`), `>0` = с timeout'ом |
|
||||||
|
|
||||||
|
Ошибки:
|
||||||
|
|
||||||
|
| Код | Когда |
|
||||||
|
|---|---|
|
||||||
|
| `WOULD_BLOCK` | `timeout_ms=0` и нет данных |
|
||||||
|
| `TIMEOUT` | За `timeout_ms` ничего не пришло |
|
||||||
|
| `DISCONNECTED` | Publisher shutdown, либо ring overrun в `STRICT_ORDER` |
|
||||||
|
|
||||||
|
`release` ACK'ает frame publisher'у (важно для `STRICT_WAIT` policy). NULL-safe. После release frame handle invalid.
|
||||||
|
|
||||||
|
## Subscriber API (async)
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef void (*cuframes_frame_callback_t)(const cuframes_frame_t *frame,
|
||||||
|
void *user_data);
|
||||||
|
typedef void (*cuframes_error_callback_t)(int err, const char *msg,
|
||||||
|
void *user_data);
|
||||||
|
|
||||||
|
int cuframes_async_subscriber_create(const cuframes_subscriber_config_t *cfg,
|
||||||
|
cuframes_frame_callback_t on_frame,
|
||||||
|
cuframes_error_callback_t on_error,
|
||||||
|
void *user_data,
|
||||||
|
cuframes_async_subscriber_t **out);
|
||||||
|
|
||||||
|
int cuframes_async_subscriber_destroy(cuframes_async_subscriber_t *sub);
|
||||||
|
```
|
||||||
|
|
||||||
|
Callback-based wrapper над sync API. Library поднимает internal thread, который сидит на `next`, вызывает `on_frame` / `on_error`, сам делает `release` после возврата из callback.
|
||||||
|
|
||||||
|
Ограничения:
|
||||||
|
|
||||||
|
- Frame **валиден только в течение callback'а** — никаких saved pointer'ов;
|
||||||
|
- Library использует internal CUDA stream, pre-wait уже выполнен — для своего stream'а используй sync API;
|
||||||
|
- `destroy` join'ит internal thread и гарантирует, что callback больше не вызовется после возврата (может занять до длительности текущего callback'а).
|
||||||
|
|
||||||
|
## Packet ring API
|
||||||
|
|
||||||
|
См. [Frame vs Packet ring](/docs/concepts/frame-vs-packet-ring) — когда нужно использовать packet ring.
|
||||||
|
|
||||||
|
### Flags
|
||||||
|
|
||||||
|
```c
|
||||||
|
#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
|
||||||
|
```
|
||||||
|
|
||||||
|
Биты соответствуют `AV_PKT_FLAG_*` у FFmpeg.
|
||||||
|
|
||||||
|
### Publisher-side
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct cuframes_packet_ring_options {
|
||||||
|
uint32_t ring_slots; /* default 64 */
|
||||||
|
uint32_t data_size; /* default 8 MiB */
|
||||||
|
uint32_t max_packet_size; /* default 2 MiB */
|
||||||
|
uint32_t codec_id; /* AV_CODEC_ID_* */
|
||||||
|
uint64_t _reserved[4];
|
||||||
|
} cuframes_packet_ring_options_t;
|
||||||
|
|
||||||
|
int cuframes_publisher_enable_packets(cuframes_publisher_t *pub,
|
||||||
|
const cuframes_packet_ring_options_t *opts);
|
||||||
|
|
||||||
|
int cuframes_publisher_set_codec_extradata(cuframes_publisher_t *pub,
|
||||||
|
const void *extradata, size_t 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);
|
||||||
|
```
|
||||||
|
|
||||||
|
`enable_packets` создаёт отдельный SHM `/dev/shm/cuframes-<key>-packets`. **Должно быть вызвано до первого `publish_packet` и желательно до того, как subscribers начнут подключаться** — иначе subscriber увидит publisher без ring'а и не получит packets. `opts=NULL` → default sizing.
|
||||||
|
|
||||||
|
`set_codec_extradata` пишет SPS/PPS/VPS bytes в shared header. Subscribers (FFmpeg demuxer) подставят это в `AVCodecContext.extradata`. Size ≤ 4096 байт.
|
||||||
|
|
||||||
|
`publish_packet` записывает один NAL unit (Annex B). На IDR обязательно ставить `CUFRAMES_PKT_FLAG_KEY` — иначе late subscriber не сможет resync'нуться.
|
||||||
|
|
||||||
|
Ошибки:
|
||||||
|
|
||||||
|
| Код | Когда |
|
||||||
|
|---|---|
|
||||||
|
| `NO_PACKET_RING` | Не вызвали `enable_packets` |
|
||||||
|
| `PACKET_OVERSIZED` | `size > max_packet_size` |
|
||||||
|
| `ALREADY_EXISTS` | (`enable_packets`) ring уже активирован |
|
||||||
|
|
||||||
|
### Subscriber-side
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct cuframes_packet cuframes_packet_t;
|
||||||
|
|
||||||
|
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);
|
||||||
|
uint64_t cuframes_packet_seq(const cuframes_packet_t *p);
|
||||||
|
|
||||||
|
int cuframes_subscriber_enable_packets(cuframes_subscriber_t *sub);
|
||||||
|
|
||||||
|
int cuframes_subscriber_next_packet(cuframes_subscriber_t *sub,
|
||||||
|
cuframes_packet_t **pkt_out,
|
||||||
|
int32_t timeout_ms);
|
||||||
|
|
||||||
|
int cuframes_subscriber_release_packet(cuframes_subscriber_t *sub,
|
||||||
|
cuframes_packet_t *pkt);
|
||||||
|
|
||||||
|
int cuframes_subscriber_get_codec_params(cuframes_subscriber_t *sub,
|
||||||
|
uint32_t *codec_id_out,
|
||||||
|
const void **extradata_out,
|
||||||
|
size_t *extradata_size_out);
|
||||||
|
```
|
||||||
|
|
||||||
|
`enable_packets` открывает второй SHM (если publisher его создал). Subscriber может одновременно иметь frames ring и packets ring, или только один из них.
|
||||||
|
|
||||||
|
`next_packet` — late subscriber на первом вызове начнёт с `last_keyframe_seq` publisher'а (decoder получит valid stream без glitches). См. [Protocol §10.14](/docs/reference/protocol).
|
||||||
|
|
||||||
|
Ошибки:
|
||||||
|
|
||||||
|
| Код | Когда |
|
||||||
|
|---|---|
|
||||||
|
| `WOULD_BLOCK` | `timeout_ms=0`, нет данных |
|
||||||
|
| `TIMEOUT` | За `timeout_ms` ничего не пришло |
|
||||||
|
| `PACKET_OVERRUN` | Subscriber отстал; library автоматически resync'нется на keyframe на next call |
|
||||||
|
| `DISCONNECTED` | Publisher shutdown |
|
||||||
|
| `NOT_FOUND` | (`enable_packets`) publisher не имеет packet ring |
|
||||||
|
|
||||||
|
`get_codec_params` возвращает pointer в library-local buffer, валидный пока subscriber жив. Если хочешь hold data past subscriber lifetime — копируй сам. Возвращает `NO_CODEC_PARAMS`, если publisher ещё не звал `set_codec_extradata`.
|
||||||
|
|
||||||
|
`release_packet` — NULL-safe. После release pointer'ы от `cuframes_packet_*` invalid.
|
||||||
|
|
||||||
|
## Utils
|
||||||
|
|
||||||
|
### Расчёт frame size
|
||||||
|
|
||||||
|
```c
|
||||||
|
int cuframes_calc_frame_size(cuframes_format_t format,
|
||||||
|
int32_t width, int32_t height,
|
||||||
|
size_t *size_out,
|
||||||
|
int32_t *pitch_y_out,
|
||||||
|
int32_t *pitch_uv_out);
|
||||||
|
```
|
||||||
|
|
||||||
|
Учитывает pitch alignment 256 байт (CUDA recommendation). `pitch_y_out` / `pitch_uv_out` опциональны (можно NULL). Возвращает `INVALID_ARG` для unknown format.
|
||||||
|
|
||||||
|
### Monotonic time
|
||||||
|
|
||||||
|
```c
|
||||||
|
int64_t cuframes_now_ns(void);
|
||||||
|
```
|
||||||
|
|
||||||
|
`CLOCK_MONOTONIC` в наносекундах. Используй как `pts_ns` для real-time pipeline'ов:
|
||||||
|
|
||||||
|
```c
|
||||||
|
cuframes_publisher_publish(pub, stream, cuframes_now_ns());
|
||||||
|
```
|
||||||
|
|
||||||
|
## См. также
|
||||||
|
|
||||||
|
- [C++ API](/docs/reference/api-cpp) — RAII wrapper.
|
||||||
|
- [Protocol reference](/docs/reference/protocol) — wire format, handshake, ABI layouts.
|
||||||
|
- [Frame vs Packet ring](/docs/concepts/frame-vs-packet-ring) — когда какой использовать.
|
||||||
|
- [Synchronization](/docs/concepts/sync-vmm-stream) — почему `cuStreamSynchronize`, а не CUDA events.
|
||||||
|
- [First publisher](/docs/getting-started/first-publisher) — рабочий C-пример.
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 2
|
||||||
|
title: C++ API
|
||||||
|
---
|
||||||
|
|
||||||
|
# C++ API reference
|
||||||
|
|
||||||
|
`<cuframes/cuframes.hpp>` — header-only RAII-wrapper над [C API](/docs/reference/api-c). Тонкий слой: handle-классы с automatic cleanup, exceptions вместо int return codes, `std::optional<FrameRef>` для `next`.
|
||||||
|
|
||||||
|
## Headers & linkage
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <cuframes/cuframes.hpp>
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# C++17 минимум (нужен std::optional)
|
||||||
|
c++ -std=c++17 app.cpp -lcuframes
|
||||||
|
# при использовании своих CUDA streams
|
||||||
|
c++ -std=c++17 app.cpp -lcuframes -lcudart
|
||||||
|
```
|
||||||
|
|
||||||
|
Header-only — самой C++ библиотеки не существует, есть только wrapper над `libcuframes.so`. ABI-совместимость наследуется от C API.
|
||||||
|
|
||||||
|
Всё в namespace `cuframes`:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
namespace cuframes {
|
||||||
|
class Error;
|
||||||
|
class Frame;
|
||||||
|
class FrameRef;
|
||||||
|
class Publisher;
|
||||||
|
class Subscriber;
|
||||||
|
class AsyncSubscriber;
|
||||||
|
struct PublisherOptions;
|
||||||
|
struct SubscriberOptions;
|
||||||
|
inline int64_t now_ns();
|
||||||
|
inline size_t calc_frame_size(...);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Исключения
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class cuframes::Error : public std::runtime_error {
|
||||||
|
public:
|
||||||
|
Error(int code, const std::string &context);
|
||||||
|
int code() const noexcept;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Бросается из всех методов, кроме explicitly `noexcept`. `code()` — оригинальный [`cuframes_error_t`](/docs/reference/api-c#error-codes); `what()` — `"<context>: <strerror>"`.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
try {
|
||||||
|
cuframes::Publisher pub({.key = "cam1", .width = 1920, .height = 1080});
|
||||||
|
} catch (const cuframes::Error &e) {
|
||||||
|
if (e.code() == CUFRAMES_ERR_ALREADY_EXISTS) {
|
||||||
|
// stale publisher с этим key
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frame & FrameRef
|
||||||
|
|
||||||
|
`Frame` — read-only non-owning view над `cuframes_frame_t`. Используется в callback'ах async subscriber'а.
|
||||||
|
|
||||||
|
`FrameRef` — RAII owning handle: `release` вызывается автоматически в destructor'е. Moveable, не copyable.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class Frame {
|
||||||
|
public:
|
||||||
|
void *cuda_ptr() const noexcept;
|
||||||
|
cuframes_format_t format() const noexcept;
|
||||||
|
int32_t width() const noexcept;
|
||||||
|
int32_t height() const noexcept;
|
||||||
|
int32_t pitch_y() const noexcept;
|
||||||
|
int32_t pitch_uv() const noexcept;
|
||||||
|
uint64_t seq() const noexcept;
|
||||||
|
int64_t pts_ns() const noexcept;
|
||||||
|
const cuframes_frame_t *raw() const noexcept;
|
||||||
|
};
|
||||||
|
|
||||||
|
class FrameRef {
|
||||||
|
public:
|
||||||
|
explicit operator bool() const noexcept;
|
||||||
|
Frame view() const noexcept;
|
||||||
|
// Shortcut accessors: cuda_ptr, width, height, pitch_y, pitch_uv, seq, pts_ns
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Все accessor-методы `noexcept` — они идут в C accessor'ы, которые ничего не аллоцируют.
|
||||||
|
|
||||||
|
## Publisher
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct PublisherOptions {
|
||||||
|
std::string key;
|
||||||
|
int32_t width = 0;
|
||||||
|
int32_t height = 0;
|
||||||
|
cuframes_format_t format = CUFRAMES_FORMAT_NV12;
|
||||||
|
int32_t ring_size = 4;
|
||||||
|
cuframes_publisher_policy_t policy = CUFRAMES_POLICY_DROP_OLDEST;
|
||||||
|
int32_t consumer_ack_timeout_ms = 0;
|
||||||
|
int32_t cuda_device = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Publisher {
|
||||||
|
public:
|
||||||
|
explicit Publisher(const PublisherOptions &opt);
|
||||||
|
|
||||||
|
// EXTERNAL ownership — DEPRECATED в v0.4, бросает Error(INVALID_ARG)
|
||||||
|
Publisher(const PublisherOptions &opt,
|
||||||
|
void *const *cuda_ptrs, int32_t ptr_count, size_t frame_size);
|
||||||
|
|
||||||
|
~Publisher();
|
||||||
|
Publisher(Publisher &&) noexcept;
|
||||||
|
Publisher &operator=(Publisher &&) noexcept;
|
||||||
|
|
||||||
|
void *acquire();
|
||||||
|
void publish(void *stream, int64_t pts_ns);
|
||||||
|
|
||||||
|
// EXTERNAL mode — DEPRECATED в v0.4
|
||||||
|
void publish_external(void *cuda_ptr, void *stream, int64_t pts_ns);
|
||||||
|
|
||||||
|
// Packet ring
|
||||||
|
void enable_packets(const cuframes_packet_ring_options_t *opts = nullptr);
|
||||||
|
void set_codec_extradata(const void *data, size_t size);
|
||||||
|
int publish_packet(const void *data, size_t size,
|
||||||
|
int64_t pts_ns, int64_t dts_ns, uint32_t flags) noexcept;
|
||||||
|
|
||||||
|
cuframes_publisher_t *raw() noexcept;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**Note про deprecated EXTERNAL ownership-конструктор.** В v0.4 второй конструктор `Publisher(opt, cuda_ptrs, ...)` под капотом вызывает `cuframes_publisher_create_external` и сразу получает `INVALID_ARG` → бросает `cuframes::Error`. Для FFmpeg filter / custom decoder integration переходи на LIBRARY mode + одна `cudaMemcpyAsync(D2D)` в acquire'нутый pointer. См. [C API note](/docs/reference/api-c#create--destroy).
|
||||||
|
|
||||||
|
`publish_packet` — единственный non-throwing метод (возвращает int). Это сделано чтобы в hot loop encoder'а не платить за exception unwind на каждом packet.
|
||||||
|
|
||||||
|
Минимальный publisher-loop:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
cuframes::Publisher pub({
|
||||||
|
.key = "cam1",
|
||||||
|
.width = 1920, .height = 1080,
|
||||||
|
.format = CUFRAMES_FORMAT_NV12,
|
||||||
|
});
|
||||||
|
|
||||||
|
cudaStream_t stream;
|
||||||
|
cudaStreamCreate(&stream);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
void *slot = pub.acquire();
|
||||||
|
// ... NVDEC / kernel пишут в slot на `stream` ...
|
||||||
|
pub.publish(stream, cuframes::now_ns());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subscriber (sync)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct SubscriberOptions {
|
||||||
|
std::string key;
|
||||||
|
std::string consumer_name; // empty = auto-generate
|
||||||
|
cuframes_subscriber_mode_t mode = CUFRAMES_MODE_NEWEST_ONLY;
|
||||||
|
int32_t cuda_device = 0;
|
||||||
|
int32_t connect_timeout_ms = 5000;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Subscriber {
|
||||||
|
public:
|
||||||
|
explicit Subscriber(const SubscriberOptions &opt);
|
||||||
|
~Subscriber();
|
||||||
|
Subscriber(Subscriber &&) noexcept;
|
||||||
|
Subscriber &operator=(Subscriber &&) noexcept;
|
||||||
|
|
||||||
|
std::optional<FrameRef> next(void *stream, int32_t timeout_ms = -1);
|
||||||
|
|
||||||
|
cuframes_subscriber_t *raw() noexcept;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`next` возвращает `std::nullopt` для recoverable conditions (`TIMEOUT`, `WOULD_BLOCK`, `DISCONNECTED`) и бросает `Error` для всего остального. Эта асимметрия сделана сознательно — три перечисленных случая ожидаемы в обычном loop'е и не должны платить за exception unwind.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
cuframes::Subscriber sub({
|
||||||
|
.key = "cam1",
|
||||||
|
.mode = CUFRAMES_MODE_NEWEST_ONLY,
|
||||||
|
});
|
||||||
|
|
||||||
|
cudaStream_t stream;
|
||||||
|
cudaStreamCreate(&stream);
|
||||||
|
|
||||||
|
while (auto frame = sub.next(stream, 1000)) {
|
||||||
|
// frame->cuda_ptr(), frame->width(), frame->pts_ns()
|
||||||
|
// release будет автоматически при выходе из scope
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Subscriber (async)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
class AsyncSubscriber {
|
||||||
|
public:
|
||||||
|
using OnFrame = std::function<void(const Frame &)>;
|
||||||
|
using OnError = std::function<void(int err, const std::string &msg)>;
|
||||||
|
|
||||||
|
AsyncSubscriber(const SubscriberOptions &opt,
|
||||||
|
OnFrame on_frame,
|
||||||
|
OnError on_error = {});
|
||||||
|
~AsyncSubscriber();
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Callback-based wrapper. Library поднимает internal thread; `Frame` валиден **только** в течение callback'а (автоматический release после return).
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
cuframes::AsyncSubscriber sub(
|
||||||
|
{.key = "cam1"},
|
||||||
|
[](const cuframes::Frame &f) {
|
||||||
|
std::printf("seq=%lu pts=%ld\n", f.seq(), f.pts_ns());
|
||||||
|
},
|
||||||
|
[](int err, const std::string &msg) {
|
||||||
|
std::fprintf(stderr, "cuframes error %d: %s\n", err, msg.c_str());
|
||||||
|
});
|
||||||
|
|
||||||
|
// держим subscriber alive...
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(60));
|
||||||
|
// destructor join'ит worker thread
|
||||||
|
```
|
||||||
|
|
||||||
|
Не copyable. Move в текущей версии тоже запрещён (поля `std::function` хранят `this`-pointer для trampoline'а — move сломает).
|
||||||
|
|
||||||
|
## Packet ring
|
||||||
|
|
||||||
|
В C++ wrapper'е packet ring доступен через прямые C-функции и `Publisher::publish_packet`. Отдельных `cuframes::Packet` / `cuframes::PacketRef` классов нет — packet API проще, и FFmpeg interop часто пишется напрямую через C.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Publisher-side
|
||||||
|
cuframes_packet_ring_options_t pkt_opts{};
|
||||||
|
pkt_opts.ring_slots = 64;
|
||||||
|
pkt_opts.data_size = 8 * 1024 * 1024;
|
||||||
|
pkt_opts.max_packet_size = 2 * 1024 * 1024;
|
||||||
|
pkt_opts.codec_id = AV_CODEC_ID_H264;
|
||||||
|
pub.enable_packets(&pkt_opts);
|
||||||
|
pub.set_codec_extradata(sps_pps.data(), sps_pps.size());
|
||||||
|
|
||||||
|
int rc = pub.publish_packet(nal, nal_size, pts, dts, CUFRAMES_PKT_FLAG_KEY);
|
||||||
|
if (rc < 0 && rc != CUFRAMES_ERR_PACKET_OVERSIZED) {
|
||||||
|
// log + skip; OVERSIZED безопасно игнорировать
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Subscriber-side — pure C-функции из `<cuframes/cuframes.h>`, см. [Packet subscriber API](/docs/reference/api-c#subscriber-side).
|
||||||
|
|
||||||
|
## Утилиты
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
inline int64_t cuframes::now_ns();
|
||||||
|
|
||||||
|
inline size_t cuframes::calc_frame_size(cuframes_format_t format,
|
||||||
|
int32_t w, int32_t h,
|
||||||
|
int32_t *pitch_y = nullptr,
|
||||||
|
int32_t *pitch_uv = nullptr);
|
||||||
|
```
|
||||||
|
|
||||||
|
`calc_frame_size` бросает `Error` на unknown format (в отличие от C-варианта, который возвращает код).
|
||||||
|
|
||||||
|
## Примеры
|
||||||
|
|
||||||
|
### Complete publisher (LIBRARY mode)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <cuframes/cuframes.hpp>
|
||||||
|
#include <cuda_runtime.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
cuframes::Publisher pub({
|
||||||
|
.key = "cam1",
|
||||||
|
.width = 1920, .height = 1080,
|
||||||
|
.format = CUFRAMES_FORMAT_NV12,
|
||||||
|
.ring_size = 4,
|
||||||
|
});
|
||||||
|
|
||||||
|
cudaStream_t stream;
|
||||||
|
cudaStreamCreate(&stream);
|
||||||
|
|
||||||
|
for (int i = 0; i < 1000; i++) {
|
||||||
|
void *slot = pub.acquire();
|
||||||
|
// ... NVDEC decode или kernel write в slot ...
|
||||||
|
pub.publish(stream, cuframes::now_ns());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Complete subscriber
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <cuframes/cuframes.hpp>
|
||||||
|
#include <cuda_runtime.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
cuframes::Subscriber sub({
|
||||||
|
.key = "cam1",
|
||||||
|
.consumer_name = "my-detector",
|
||||||
|
.mode = CUFRAMES_MODE_NEWEST_ONLY,
|
||||||
|
.connect_timeout_ms = 5000,
|
||||||
|
});
|
||||||
|
|
||||||
|
cudaStream_t stream;
|
||||||
|
cudaStreamCreate(&stream);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
auto frame = sub.next(stream, 1000);
|
||||||
|
if (!frame) continue; // timeout/disconnect
|
||||||
|
|
||||||
|
// frame->cuda_ptr() — VRAM pointer
|
||||||
|
// frame->width(), frame->height() — pixels
|
||||||
|
// frame->pitch_y(), frame->pitch_uv() — байт на строку
|
||||||
|
// ... ML inference / CUDA filter на stream ...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Async with lambdas
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <cuframes/cuframes.hpp>
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::atomic<uint64_t> frames{0};
|
||||||
|
|
||||||
|
cuframes::AsyncSubscriber sub(
|
||||||
|
{.key = "cam1"},
|
||||||
|
[&](const cuframes::Frame &f) {
|
||||||
|
frames.fetch_add(1);
|
||||||
|
// pre-sync уже выполнен library-side; f.cuda_ptr() ready
|
||||||
|
});
|
||||||
|
|
||||||
|
std::this_thread::sleep_for(std::chrono::seconds(10));
|
||||||
|
std::printf("processed %lu frames\n", frames.load());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## См. также
|
||||||
|
|
||||||
|
- [C API](/docs/reference/api-c) — underlying C-функции.
|
||||||
|
- [Protocol reference](/docs/reference/protocol) — wire format spec.
|
||||||
|
- [First publisher](/docs/getting-started/first-publisher) — minimal end-to-end пример.
|
||||||
@@ -0,0 +1,608 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 3
|
||||||
|
title: Спецификация wire protocol v4
|
||||||
|
---
|
||||||
|
|
||||||
|
# cuframes wire protocol — version 4
|
||||||
|
|
||||||
|
**Status:** v4 — production (deployed 2026-05-25). Semver-stable в пределах major.
|
||||||
|
**Endianness:** little-endian (CUDA-platforms only).
|
||||||
|
**libcuframes version:** 0.4.0.
|
||||||
|
|
||||||
|
Эта страница описывает **byte-exact** формат всех структур и сообщений cuframes. Любая реализация (C, Python ctypes, Rust bindings, FFmpeg plugin) должна соответствовать этому документу. Reference implementation — `libcuframes/src/` в репозитории.
|
||||||
|
|
||||||
|
## Что изменилось в v4
|
||||||
|
|
||||||
|
В v0.4 заменили механизм sharing'а GPU-памяти с CUDA IPC mem-handles на CUDA VMM + POSIX file descriptors. Это **breaking change** на wire level.
|
||||||
|
|
||||||
|
| Аспект | v1–v3 (legacy) | v4 (current) |
|
||||||
|
|---|---|---|
|
||||||
|
| Magic | `0xCC7C1DCC` | `0xCC7C1DCE` |
|
||||||
|
| Protocol version | 1–3 | 4 |
|
||||||
|
| GPU memory share | `cudaIpcMemHandle_t` в slot descriptor | `cuMemCreate(POSIX_FILE_DESCRIPTOR)` + `SCM_RIGHTS` |
|
||||||
|
| Cross-process sync | `cudaIpcEventHandle_t` + `cudaStreamWaitEvent` | producer's `cuStreamSynchronize` + atomic seq release |
|
||||||
|
| PID namespace sharing | required (ограничение CUDA IPC) | **not required** — POSIX FD работает поверх SCM_RIGHTS |
|
||||||
|
| Handshake messages | `HELLO` → `SUBSCRIBE` | `HELLO` → `SUBSCRIBE` → `VMM_FDS` (new) |
|
||||||
|
| EXTERNAL ownership | поддерживался | удалён (VMM требует cuMemCreate-memory) |
|
||||||
|
|
||||||
|
Старый magic `0xCC7C1DCC` приводит к clean fail в v4 (`HELLO_RESP(result=CUFRAMES_ERR_PROTOCOL)`). Mixed-version deployment **не работает** — обновлять надо publisher и всех subscribers одновременно.
|
||||||
|
|
||||||
|
См. [Synchronization & VMM stream](/docs/concepts/sync-vmm-stream) — почему именно stream sync, а не CUDA events.
|
||||||
|
|
||||||
|
## 1. Ресурсы / Lifecycle
|
||||||
|
|
||||||
|
Один publisher создаёт следующие kernel-level ресурсы:
|
||||||
|
|
||||||
|
| Ресурс | Path | Назначение | Cleanup |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Unix socket | `/run/cuframes/<key>.sock` | Handshake + control plane | unlink при `destroy()`; orphaned после crash — cleanup'ится при следующем `create` через `O_EXCL` retry |
|
||||||
|
| Frame SHM | `/dev/shm/cuframes-<key>` | Frame ring header + slot descriptors | `shm_unlink` при `destroy()`; orphaned автоматически, если nobody mmap'ит |
|
||||||
|
| VMM-allocated VRAM | (no path; FD получают subscribers через `SCM_RIGHTS`) | Frame data в HBM | освобождается, когда last subscriber `cuMemUnmap` + publisher `cuMemRelease` |
|
||||||
|
| Packet SHM | `/dev/shm/cuframes-<key>-packets` | Packet ring header + slots + data section | `shm_unlink` при `destroy()`; opt-in (только если вызван `enable_packets`) |
|
||||||
|
|
||||||
|
`<key>` — ASCII, `[a-zA-Z0-9_-]`, 1–63 байт. Library валидирует regex `^[a-zA-Z0-9_-]{1,63}$`.
|
||||||
|
|
||||||
|
### Normal shutdown
|
||||||
|
|
||||||
|
1. `cuframes_publisher_destroy()` вызван.
|
||||||
|
2. Publisher шлёт всем connected subscribers `SHUTDOWN` через socket.
|
||||||
|
3. Subscribers закрывают VMM mappings (`cuMemUnmap`, `cuMemAddressFree`, `close(fd)`), socket, munmap, возвращают `DISCONNECTED`.
|
||||||
|
4. Publisher: `cuMemUnmap` own pool + `cuMemRelease`, close socket, unlink socket + shm.
|
||||||
|
|
||||||
|
### Abnormal shutdown (publisher crash)
|
||||||
|
|
||||||
|
Producer не успевает unlink. Stale socket и shm остаются. Recovery при следующем `cuframes_publisher_create()`:
|
||||||
|
|
||||||
|
1. Попытка `connect(sock_path)` → `ECONNREFUSED` → stale, unlink.
|
||||||
|
2. Открытие `/dev/shm/cuframes-<key>`: если magic совпадает, проверка liveness через `pidfd_open(producer_pid)` или `kill(pid, 0)`.
|
||||||
|
3. Если producer dead → `shm_unlink` + продолжаем create.
|
||||||
|
4. Если producer жив → return `ALREADY_EXISTS`.
|
||||||
|
|
||||||
|
Subscribers детектят publisher crash через socket EOF (recv returns 0) или `SIGPIPE` на send. После этого они должны `cuMemUnmap` все slot mappings и вернуть `DISCONNECTED` в `cuframes_subscriber_next`.
|
||||||
|
|
||||||
|
**Note про VMM leak risk.** В отличие от CUDA IPC handles, POSIX FDs автоматически очищаются ядром при `close()` процесса. Даже если subscriber падает unclean'но — kernel сам close'нет FD, и `cuMemUnmap` отработает в driver'е при последующем `cuMemRelease` publisher'а. В v0.4 это менее опасно, чем было в legacy v1–v3.
|
||||||
|
|
||||||
|
## 2. Frame SHM layout
|
||||||
|
|
||||||
|
`/dev/shm/cuframes-<key>` имеет фиксированный размер: `sizeof(cuframes_shared_header_t)`. Без variable-length секций.
|
||||||
|
|
||||||
|
### 2.0 Header byte layout
|
||||||
|
|
||||||
|
```
|
||||||
|
Offset Size Field Comments
|
||||||
|
─────── ────── ──────────────────────── ────────────────────────────────────────
|
||||||
|
0x0000 4 magic (LE u32) 0xCC7C1DCE
|
||||||
|
0x0004 4 proto_version (LE u32) 4
|
||||||
|
0x0008 4 lib_version_major
|
||||||
|
0x000C 4 lib_version_minor
|
||||||
|
0x0010 4 lib_version_patch
|
||||||
|
0x0014 4 reserved_a 0
|
||||||
|
0x0018 8 producer_pid (LE u64) для liveness check
|
||||||
|
0x0020 8 ring_size (LE u64) 1..16
|
||||||
|
0x0028 8 ownership_mode 0 (LIBRARY only в v4)
|
||||||
|
0x0030 8 policy 0=DROP_OLDEST, 1=STRICT_WAIT
|
||||||
|
0x0038 8 max_subscribers 32 (захардкожено)
|
||||||
|
0x0040 64 meta frame meta packed (см. §2.1)
|
||||||
|
0x0080 64 reserved_events 0 (был ipc_event_handle в v1–v3)
|
||||||
|
0x00C0 8 global_seq (LE u64) atomic, монотонная
|
||||||
|
0x00C8 8 subscriber_bitmap atomic, bit per subscriber slot
|
||||||
|
0x00D0 8 shutdown_flag atomic, 0=normal, 1=shutting down
|
||||||
|
0x00D8 40 reserved_b 0
|
||||||
|
0x0100 N×D slots[ring_size] slot descriptor, см. §2.2
|
||||||
|
0x0100+ND M×S subscribers[32] subscriber slot, см. §2.3
|
||||||
|
```
|
||||||
|
|
||||||
|
Constants:
|
||||||
|
|
||||||
|
- N = `ring_size`, до 16
|
||||||
|
- D = `sizeof(slot_descriptor)` = **192 байт** (§2.2)
|
||||||
|
- M = 32 (max subscribers)
|
||||||
|
- S = `sizeof(subscriber_slot)` = 128 байт (§2.3)
|
||||||
|
- Max SHM size: `0x0100 + 16×192 + 32×128` ≈ 7.3 KB
|
||||||
|
|
||||||
|
Все atomic-помеченные поля доступны через C11 `_Atomic` (или `__atomic_*`).
|
||||||
|
|
||||||
|
### 2.1 Frame meta (64 байта)
|
||||||
|
|
||||||
|
```
|
||||||
|
Offset Size Field Comments
|
||||||
|
0x00 4 format (LE u32) cuframes_format_t enum
|
||||||
|
0x04 4 width
|
||||||
|
0x08 4 height
|
||||||
|
0x0C 4 pitch_y байт на строку Y / single plane
|
||||||
|
0x10 4 pitch_uv байт на UV (0 если no chroma plane)
|
||||||
|
0x14 4 bits_per_pixel информативно
|
||||||
|
0x18 8 frame_size_bytes полный размер (Y+UV или packed)
|
||||||
|
0x20 32 reserved 0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 Slot descriptor (192 байта)
|
||||||
|
|
||||||
|
```
|
||||||
|
Offset Size Field Comments
|
||||||
|
0x00 8 seq (LE u64, atomic) published seq; UINT64_MAX = invalid
|
||||||
|
0x08 8 pts_ns (LE i64, atomic)
|
||||||
|
0x10 8 ack_bitmap (LE u64, atomic) bit i = subscriber #i ACK'нул
|
||||||
|
0x18 8 written_bytes для diagnostics (может быть 0)
|
||||||
|
0x20 64 reserved_mem_handle 0 в v4 (был cudaIpcMemHandle_t в v1–v3)
|
||||||
|
0x60 32 reserved_external 0 в v4 (был cuda_ptr_external)
|
||||||
|
0x80 16 reserved_a
|
||||||
|
0x90 48 reserved_b
|
||||||
|
0xC0 END
|
||||||
|
```
|
||||||
|
|
||||||
|
В v4 поле `mem_handle` (offset 0x20, 64 байта) **больше не используется** — вместо IPC handle subscribers получают POSIX FD через `SCM_RIGHTS` во время handshake (см. §3). Поле зарезервировано для возможного re-use в будущих версиях. Reader v4 должен игнорировать его содержимое.
|
||||||
|
|
||||||
|
Slot — статичный в плане memory layout (создаётся в publisher_create), но `seq` / `pts_ns` / `ack_bitmap` / `written_bytes` обновляются атомарно на каждом publish.
|
||||||
|
|
||||||
|
### 2.3 Subscriber slot (128 байт)
|
||||||
|
|
||||||
|
```
|
||||||
|
Offset Size Field Comments
|
||||||
|
0x00 8 state (LE u64, atomic) 0=free, 1=connecting, 2=active, 3=draining
|
||||||
|
0x08 8 consumer_pid (LE u64) liveness check
|
||||||
|
0x10 8 last_seen_seq (LE u64) monotonic — последний ACK'нутый seq
|
||||||
|
0x18 8 last_ack_ns (LE i64) wall-clock последнего ACK
|
||||||
|
0x20 32 consumer_name ASCII zero-terminated, max 31 char
|
||||||
|
0x40 64 reserved 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Bit-position **0** зарезервирован — sentinel. Используются bits 1..31 → max 31 subscriber'ов (errcode `TOO_MANY` при попытке 32-го).
|
||||||
|
|
||||||
|
## 3. Unix socket protocol
|
||||||
|
|
||||||
|
Publisher `listen()`'ит SOCK_STREAM по `/run/cuframes/<key>.sock`. Subscriber `connect()`'ится.
|
||||||
|
|
||||||
|
Socket используется для:
|
||||||
|
|
||||||
|
- Handshake (`HELLO`, `SUBSCRIBE`)
|
||||||
|
- **VMM file descriptor delivery** (`VMM_FDS`, новое в v4) через `sendmsg(SCM_RIGHTS)`
|
||||||
|
- Lifetime signals (`SHUTDOWN`, force-disconnect, `UNSUBSCRIBE`)
|
||||||
|
|
||||||
|
Socket **не** используется для frame transfer — это VMM-mapped shared memory + atomic `global_seq`.
|
||||||
|
|
||||||
|
### 3.1 Framing
|
||||||
|
|
||||||
|
Каждое сообщение — TLV (type-length-value):
|
||||||
|
|
||||||
|
```
|
||||||
|
[4 bytes] message_type (LE u32, см. §3.2)
|
||||||
|
[4 bytes] payload_length (LE u32, bytes excl. these 8 header bytes)
|
||||||
|
[N bytes] payload (длина = payload_length)
|
||||||
|
```
|
||||||
|
|
||||||
|
Для `VMM_FDS` сопровождается ancillary data через `sendmsg` (см. §3.7).
|
||||||
|
|
||||||
|
### 3.2 Message types
|
||||||
|
|
||||||
|
| ID | Name | Direction | Когда |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `0x01` | `HELLO_REQ` | C→P | Первое сообщение от consumer |
|
||||||
|
| `0x02` | `HELLO_RESP` | P→C | Ответ publisher'а |
|
||||||
|
| `0x03` | `SUBSCRIBE_REQ` | C→P | Завершение handshake, выделить subscriber slot |
|
||||||
|
| `0x04` | `SUBSCRIBE_RESP` | P→C | Подтверждение + assigned bit |
|
||||||
|
| `0x05` | `VMM_FDS` | P→C | **(new в v4)** Передача N file descriptors через SCM_RIGHTS |
|
||||||
|
| `0x10` | `UNSUBSCRIBE` | C→P | Graceful disconnect |
|
||||||
|
| `0x30` | `SHUTDOWN` | P→C | Publisher shutting down |
|
||||||
|
| `0xF0` | `PING` | both | Liveness check |
|
||||||
|
| `0xF1` | `PONG` | both | Reply to PING |
|
||||||
|
| `0xFE` | `ERROR` | both | Error notification (см. §3.9) |
|
||||||
|
|
||||||
|
`0x20` (`EVENT_FD` в legacy v1–v3) — **deprecated в v4**. Wakeup сейчас делается через atomic polling `global_seq`; reintroduce через FD wakeup channel — возможная фича v0.5.
|
||||||
|
|
||||||
|
### 3.3 HELLO_REQ payload (consumer → publisher)
|
||||||
|
|
||||||
|
```
|
||||||
|
[4 bytes] proto_version (LE u32) consumer's wire version (must be 4)
|
||||||
|
[4 bytes] consumer_name_len (LE u32)
|
||||||
|
[N bytes] consumer_name (UTF-8, без null-terminator)
|
||||||
|
[4 bytes] cuda_device (LE i32)
|
||||||
|
[4 bytes] mode (LE u32, cuframes_subscriber_mode_t)
|
||||||
|
[12 bytes] reserved (must be 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 HELLO_RESP payload (publisher → consumer)
|
||||||
|
|
||||||
|
```
|
||||||
|
[4 bytes] result (LE i32) 0 = success, negative = cuframes_error_t
|
||||||
|
[4 bytes] proto_version_actual (LE u32) publisher's wire version
|
||||||
|
[4 bytes] ring_size (LE u32)
|
||||||
|
[4 bytes] ownership_mode (LE u32) 0 (LIBRARY)
|
||||||
|
[64 bytes] frame_meta см. §2.1
|
||||||
|
[4 bytes] shm_path_len (LE u32)
|
||||||
|
[N bytes] shm_path (UTF-8, ASCII subset)
|
||||||
|
[12 bytes] reserved
|
||||||
|
```
|
||||||
|
|
||||||
|
При `result < 0` payload может быть короче (только `result` + `reserved`).
|
||||||
|
|
||||||
|
### 3.5 SUBSCRIBE_REQ payload
|
||||||
|
|
||||||
|
```
|
||||||
|
[4 bytes] proto_version (повтор, для double-check)
|
||||||
|
[12 bytes] reserved
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.6 SUBSCRIBE_RESP payload
|
||||||
|
|
||||||
|
```
|
||||||
|
[4 bytes] result (LE i32)
|
||||||
|
[4 bytes] assigned_bit (LE u32) 1..31
|
||||||
|
[8 bytes] initial_seq (LE u64) текущий global_seq на момент handshake
|
||||||
|
[12 bytes] reserved
|
||||||
|
```
|
||||||
|
|
||||||
|
После SUBSCRIBE_RESP publisher сразу отправляет `VMM_FDS` (§3.7) — это часть handshake'а, subscriber должен ожидать его перед переходом в READY.
|
||||||
|
|
||||||
|
### 3.7 VMM_FDS — file descriptor delivery (v4)
|
||||||
|
|
||||||
|
Publisher отправляет `ring_size` file descriptors через `sendmsg()` с ancillary data (`SCM_RIGHTS`).
|
||||||
|
|
||||||
|
**TLV payload (без ancillary):**
|
||||||
|
|
||||||
|
```
|
||||||
|
[4 bytes] fd_count (LE u32) должно = ring_size
|
||||||
|
[8 bytes] frame_size_bytes (LE u64) size одного VMM mapping
|
||||||
|
[8 bytes] vmm_granularity (LE u64) округление от CUDA driver (обычно 2 MiB)
|
||||||
|
[12 bytes] reserved (must be 0)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ancillary data:** `cmsg` уровня `SOL_SOCKET`, type `SCM_RIGHTS`, contains `fd_count × sizeof(int)` file descriptors. Kernel дублирует FDs в consumer process автоматически.
|
||||||
|
|
||||||
|
Consumer обязан:
|
||||||
|
|
||||||
|
1. `recvmsg` с buffer'ом для cmsg достаточного размера (рекомендуется `CMSG_SPACE(sizeof(int) * 16)`);
|
||||||
|
2. Для каждого FD: `cuMemImportFromShareableHandle(&handle, fd, CU_MEM_HANDLE_TYPE_POSIX_FILE_DESCRIPTOR, 0)`;
|
||||||
|
3. `cuMemAddressReserve` → `cuMemMap` → `cuMemSetAccess` (READ_WRITE на consumer device);
|
||||||
|
4. `close(fd)` (CUDA driver держит свою reference после import).
|
||||||
|
|
||||||
|
После успешного импорта всех FDs subscriber переходит в READY state.
|
||||||
|
|
||||||
|
### 3.8 SHUTDOWN payload
|
||||||
|
|
||||||
|
```
|
||||||
|
[4 bytes] reason (LE u32) 0 = normal, 1 = error, 2 = upgrade
|
||||||
|
[12 bytes] reserved
|
||||||
|
```
|
||||||
|
|
||||||
|
Subscriber на SHUTDOWN → `cuMemUnmap`/`cuMemAddressFree` на всех slots → возвращает `DISCONNECTED` user'у на next call.
|
||||||
|
|
||||||
|
### 3.9 ERROR payload
|
||||||
|
|
||||||
|
```
|
||||||
|
[4 bytes] error_code (LE i32) cuframes_error_t
|
||||||
|
[4 bytes] message_len (LE u32)
|
||||||
|
[N bytes] message (UTF-8)
|
||||||
|
[12 bytes] reserved
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. Handshake sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
participant C as Subscriber (consumer)
|
||||||
|
participant P as Publisher
|
||||||
|
|
||||||
|
C->>P: socket connect /run/cuframes/cam1.sock
|
||||||
|
C->>P: HELLO_REQ (proto_version=4, consumer_name, cuda_device, mode)
|
||||||
|
|
||||||
|
alt proto mismatch / format unsupported
|
||||||
|
P-->>C: HELLO_RESP (result=ERR_PROTOCOL or ERR_FORMAT)
|
||||||
|
Note over C,P: socket close, subscriber returns error
|
||||||
|
else handshake ok
|
||||||
|
P-->>C: HELLO_RESP (result=0, ring_size, frame_meta, shm_path)
|
||||||
|
C->>P: SUBSCRIBE_REQ (proto_version=4)
|
||||||
|
|
||||||
|
alt subscriber slot pool full
|
||||||
|
P-->>C: SUBSCRIBE_RESP (result=ERR_TOO_MANY)
|
||||||
|
else slot allocated
|
||||||
|
P-->>C: SUBSCRIBE_RESP (result=0, assigned_bit, initial_seq)
|
||||||
|
P-->>C: VMM_FDS (fd_count, frame_size, granularity) + SCM_RIGHTS [fd0..fdN]
|
||||||
|
Note over C: cuMemImportFromShareableHandle for each fd<br/>cuMemAddressReserve + Map + SetAccess
|
||||||
|
Note over C,P: READY — subscriber may poll global_seq
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
Sequence для legacy v1–v3 не имеет шага `VMM_FDS` — там slot descriptor в SHM сам нёс `cudaIpcMemHandle_t`, и subscriber делал `cudaIpcOpenMemHandle` без socket interaction. В v4 этот шаг **обязателен** — без `VMM_FDS` subscriber не получит pointer'ы и не сможет читать frames.
|
||||||
|
|
||||||
|
## 5. Sync model (v4)
|
||||||
|
|
||||||
|
В v0.4 cross-process synchronization упрощена до stream sync на стороне producer'а:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Publisher side (libcuframes/src/producer.c)
|
||||||
|
cuStreamSynchronize(stream); // ensure all writes coherent
|
||||||
|
atomic_store_release(&slot.seq, next_seq);
|
||||||
|
atomic_store_release(&hdr.global_seq, next_seq);
|
||||||
|
|
||||||
|
// Consumer side (libcuframes/src/consumer.c)
|
||||||
|
uint64_t cur = atomic_load_acquire(&hdr.global_seq);
|
||||||
|
if (cur > my_last) {
|
||||||
|
// read slot metadata, do cudaMemcpyAsync(DtoD, consumer_stream)
|
||||||
|
// HW coherence на одном GPU — no event wait нужен
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Почему так:
|
||||||
|
|
||||||
|
- VMM-shared memory на одном GPU — single physical HBM region. Producer's `cuStreamSynchronize` гарантирует, что writes завершились в HBM. Consumer reads после atomic-release-acquire pair видят валидные данные без CUDA event roundtrip.
|
||||||
|
- Это **не работает cross-GPU** — для multi-GPU нужен `cuMemSetAccess` на оба device'а + явные events. v4 не поддерживает cross-device VMM share (ограничение CUDA driver).
|
||||||
|
- Producer overhead `cuStreamSynchronize` ≈ 1 мс на frame при 25 fps — измеримо, но приемлемо.
|
||||||
|
|
||||||
|
Подробнее: [Synchronization & VMM stream](/docs/concepts/sync-vmm-stream).
|
||||||
|
|
||||||
|
## 6. ACK protocol
|
||||||
|
|
||||||
|
При публикации slot N publisher:
|
||||||
|
|
||||||
|
1. Записывает frame data в acquire'нутый slot;
|
||||||
|
2. `cuStreamSynchronize(stream)` — coherence barrier;
|
||||||
|
3. Atomic: `slot[N].seq = next_seq`, `slot[N].pts_ns = now`, `slot[N].ack_bitmap = 0`;
|
||||||
|
4. Atomic RELEASE: `global_seq = next_seq`.
|
||||||
|
|
||||||
|
Subscriber:
|
||||||
|
|
||||||
|
1. ACQUIRE load `global_seq`. Если новое — process;
|
||||||
|
2. Process frame (CUDA kernel, DtoD copy, etc.) на consumer_stream;
|
||||||
|
3. ACK: `atomic_fetch_or(&slot.ack_bitmap, 1ULL << my_bit, RELEASE)`;
|
||||||
|
4. Atomic store `subscriber_slot.last_seen_seq = seq`, `last_ack_ns = now`.
|
||||||
|
|
||||||
|
Publisher (STRICT_WAIT mode) перед next publish ждёт:
|
||||||
|
|
||||||
|
- `ack_bitmap == subscriber_bitmap` (все active subscribers ACK'нули), или
|
||||||
|
- timeout `consumer_ack_timeout_ms` истёк → mark dead subscriber, clear его bit.
|
||||||
|
|
||||||
|
Publisher (DROP_OLDEST mode) — не ждёт ACK, просто перезаписывает slot.
|
||||||
|
|
||||||
|
## 7. Versioning rules
|
||||||
|
|
||||||
|
### 7.1 Wire protocol version
|
||||||
|
|
||||||
|
`proto_version` — single integer. Текущий — `4`. Breaking changes → bump.
|
||||||
|
|
||||||
|
Handshake:
|
||||||
|
|
||||||
|
- Если `subscriber.proto_version != publisher.proto_version` → `HELLO_RESP(result=ERR_PROTOCOL)`. **В v4 нет backward-compat layer'а** с v1–v3 — magic другой, mem-handle scheme другая.
|
||||||
|
|
||||||
|
### 7.2 Library version (semver)
|
||||||
|
|
||||||
|
`lib_version_major.minor.patch` — informational, не используется для compat-decisions. Передаётся в SHM header для diagnostics.
|
||||||
|
|
||||||
|
### 7.3 Reserved fields
|
||||||
|
|
||||||
|
Все `reserved_*` — должны быть `0` при write, reader игнорирует. Это позволяет в minor-релизах добавлять fields в reserved space без breaking ABI.
|
||||||
|
|
||||||
|
## 8. Conformance fixture
|
||||||
|
|
||||||
|
Test skeleton (Phase 1, в `tests/conformance/`):
|
||||||
|
|
||||||
|
```c
|
||||||
|
TEST(ProtocolLayout, ShmHeaderMagic) {
|
||||||
|
EXPECT_EQ(CUFRAMES_PROTOCOL_MAGIC_V4, 0xCC7C1DCE);
|
||||||
|
EXPECT_EQ(cuframes_protocol_version(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ProtocolLayout, ShmHeaderOffsets) {
|
||||||
|
EXPECT_EQ(offsetof(cuframes_shared_header_t, magic), 0x0000);
|
||||||
|
EXPECT_EQ(offsetof(cuframes_shared_header_t, proto_version), 0x0004);
|
||||||
|
EXPECT_EQ(offsetof(cuframes_shared_header_t, meta), 0x0040);
|
||||||
|
EXPECT_EQ(offsetof(cuframes_shared_header_t, global_seq), 0x00C0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ProtocolLayout, SlotDescriptorSize) {
|
||||||
|
EXPECT_EQ(sizeof(cuframes_slot_descriptor_t), 192);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ProtocolLayout, SubscriberSlotSize) {
|
||||||
|
EXPECT_EQ(sizeof(cuframes_subscriber_slot_t), 128);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(Handshake, VmmFdsDelivered) {
|
||||||
|
// Setup publisher with ring_size=4
|
||||||
|
// Connect subscriber, complete HELLO+SUBSCRIBE
|
||||||
|
// Expect: VMM_FDS message with fd_count=4 + 4 valid FDs in SCM_RIGHTS
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 9. Reference implementation
|
||||||
|
|
||||||
|
`libcuframes/src/` в repo:
|
||||||
|
|
||||||
|
- `producer.c` — VMM allocation, FD send, atomic publish
|
||||||
|
- `consumer.c` — FD receive, VMM import, atomic read
|
||||||
|
- `protocol.c` — TLV framing, `send_msg_with_fds` / `recv_msg_with_fds` helpers
|
||||||
|
|
||||||
|
Любая другая реализация (Python ctypes, Rust bindings, FFmpeg plugin) должна быть conformance-tested против этого документа.
|
||||||
|
|
||||||
|
## 10. Packet ring (proto_version остаётся 4)
|
||||||
|
|
||||||
|
Packet ring — отдельный SHM `/dev/shm/cuframes-<key>-packets` с собственным magic `0xCC7C1DCD`. **Не использует CUDA** (encoded data на CPU, без GPU sync). VMM-изменения v0.4 packet ring **не затронули** — wire format и layout совпадают с v0.2 спецификацией.
|
||||||
|
|
||||||
|
### 10.1 Совместимость с frames ring
|
||||||
|
|
||||||
|
- Subscriber может запросить только frames, только packets, или оба (см. §10.9 — subscribe flags).
|
||||||
|
- Packet ring опционален — если publisher не вызвал `enable_packets`, subscriber'ам `enable_packets` вернёт `NOT_FOUND`.
|
||||||
|
|
||||||
|
### 10.2 Packet SHM layout
|
||||||
|
|
||||||
|
Размер: `sizeof(packet_ring_header_t) + N×PSE + DATA_SIZE`, где:
|
||||||
|
|
||||||
|
- N = `ring_slots`, default 64 (1..1024)
|
||||||
|
- PSE = `sizeof(packet_slot_entry_t)` = 64 байт (§10.5)
|
||||||
|
- DATA_SIZE = `data_size`, default 8 MB
|
||||||
|
|
||||||
|
#### Header byte layout
|
||||||
|
|
||||||
|
```
|
||||||
|
Offset Size Field Comments
|
||||||
|
─────────────────────── ────── ────────────────────────── ────────────────────────────
|
||||||
|
0x0000 4 magic (LE u32) 0xCC7C1DCD (frames magic + (−1))
|
||||||
|
0x0004 4 proto_version (LE u32) 4 (совпадает с frames в v0.4)
|
||||||
|
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
|
||||||
|
0x0014 4 codec_extradata_size ≤ 4096
|
||||||
|
0x0018 8 producer_pid (LE u64)
|
||||||
|
0x0020 8 global_seq (LE u64, atomic) монотонная по packets
|
||||||
|
0x0028 8 last_keyframe_seq (atomic) для late subscribers
|
||||||
|
0x0030 8 write_offset (LE u64, atom) текущий cursor в data ring
|
||||||
|
0x0038 8 shutdown_flag (atomic)
|
||||||
|
0x0040 4096 codec_extradata SPS/PPS/VPS bytes
|
||||||
|
0x1040 N×64 slots[N] packet_slot_entry_t
|
||||||
|
0x1040+N×64 DATA_SIZE data[] wraparound byte buffer
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.3 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` (27), `AV_CODEC_ID_HEVC` (173), `AV_CODEC_ID_AV1`. Subscriber пишет extradata в `AVCodecContext.extradata` своего decoder'а или в `AVStream.codecpar->extradata` для muxer'ов.
|
||||||
|
|
||||||
|
Extradata устанавливается publisher'ом **один раз** при первом keyframe (или из RTSP SDP). После — fixed на lifetime publisher'а (codec change mid-stream → destroy + recreate с новым `<key>`).
|
||||||
|
|
||||||
|
### 10.4 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
|
||||||
|
0x18 8 data_offset (LE u64) absolute byte cursor в data[]
|
||||||
|
0x20 4 data_size (LE u32) payload size
|
||||||
|
0x24 4 flags (LE u32) см. §10.5
|
||||||
|
0x28 24 reserved 0
|
||||||
|
```
|
||||||
|
|
||||||
|
`data_offset` — absolute byte cursor (может расти неограниченно), фактический byte index = `data_offset % data_size`. Subscriber'у возможно понадобится split read при wraparound.
|
||||||
|
|
||||||
|
### 10.5 Packet flags
|
||||||
|
|
||||||
|
| Бит | Name | Комментарий |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | `KEY` | keyframe (IDR for H.264, CRA/IDR for HEVC). **Critical** для late subscribers. |
|
||||||
|
| 1 | `CORRUPT` | publisher детектнул damaged packet (RTP loss) — subscriber может skip |
|
||||||
|
| 2 | `DISCONTINUITY` | был gap перед этим packet (publisher reconnect к камере) |
|
||||||
|
| 3 | `LAST_IN_AU` | last NAL в access unit — для muxer'ов, которые ждут полный frame |
|
||||||
|
| 4–31 | reserved | 0 |
|
||||||
|
|
||||||
|
Маппинг в `AVPacket.flags`:
|
||||||
|
|
||||||
|
- bit 0 → `AV_PKT_FLAG_KEY`
|
||||||
|
- bit 1 → `AV_PKT_FLAG_CORRUPT`
|
||||||
|
- bit 2 → `AV_PKT_FLAG_DISCONTINUITY` (FFmpeg 5+)
|
||||||
|
|
||||||
|
### 10.6 Atomic publish (pseudo-C)
|
||||||
|
|
||||||
|
```c
|
||||||
|
uint64_t seq = atomic_load(&hdr->global_seq, RELAXED) + 1;
|
||||||
|
uint64_t off = atomic_load(&hdr->write_offset, RELAXED);
|
||||||
|
|
||||||
|
size_t slot_idx = seq % hdr->ring_slots;
|
||||||
|
packet_slot_entry_t *slot = &slots[slot_idx];
|
||||||
|
|
||||||
|
// Wraparound write
|
||||||
|
size_t off_in_ring = off % hdr->data_size;
|
||||||
|
size_t first = min(size, hdr->data_size - off_in_ring);
|
||||||
|
memcpy(data + off_in_ring, payload, first);
|
||||||
|
if (first < size)
|
||||||
|
memcpy(data, payload + first, size - first);
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
atomic_store(&hdr->write_offset, off + size, RELEASE);
|
||||||
|
atomic_store(&hdr->global_seq, seq, RELEASE);
|
||||||
|
|
||||||
|
if (flags & PKT_FLAG_KEY)
|
||||||
|
atomic_store(&hdr->last_keyframe_seq, seq, RELEASE);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.7 Atomic read (pseudo-C)
|
||||||
|
|
||||||
|
```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 — re-anchor on last keyframe
|
||||||
|
want_seq = atomic_load(&hdr->last_keyframe_seq, ACQUIRE);
|
||||||
|
return PACKET_OVERRUN;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy payload (wraparound aware)
|
||||||
|
uint64_t off = slot->data_offset % hdr->data_size;
|
||||||
|
uint32_t size = slot->data_size;
|
||||||
|
uint32_t first = min(size, hdr->data_size - off);
|
||||||
|
memcpy(out, data + off, first);
|
||||||
|
if (first < size)
|
||||||
|
memcpy(out + first, data, size - first);
|
||||||
|
|
||||||
|
// Re-check (seqlock variant) — защита от overrun mid-read
|
||||||
|
if (atomic_load(&slot->seq, ACQUIRE) != want_seq)
|
||||||
|
return PACKET_OVERRUN;
|
||||||
|
|
||||||
|
my_last_seq = want_seq;
|
||||||
|
return OK;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.8 Late subscriber → keyframe-aligned start
|
||||||
|
|
||||||
|
При `cuframes_subscriber_enable_packets()` subscriber устанавливает `my_last_seq = last_keyframe_seq - 1` (читая `last_keyframe_seq` из header'а). Первый `next_packet` вернёт keyframe (decoder может start без glitches).
|
||||||
|
|
||||||
|
**Risk:** если в момент enable_packets `last_keyframe_seq` уже выехал из ring (slow start subscriber, GOP > ring_slots packets) — subscriber детектит overrun в первом read, и library переходит на следующий keyframe.
|
||||||
|
|
||||||
|
Sizing guide: `packet_ring_slots × avg_packet_size > GOP_size_in_bytes` для нормальной работы.
|
||||||
|
|
||||||
|
### 10.9 Socket protocol extensions для packets
|
||||||
|
|
||||||
|
#### HELLO_REQ — subscribe flags в reserved field
|
||||||
|
|
||||||
|
v4 интерпретирует первые 4 байта `reserved` (offset HELLO_REQ + 28) как `subscribe_flags`:
|
||||||
|
|
||||||
|
| Бит | Name | Комментарий |
|
||||||
|
|---|---|---|
|
||||||
|
| 0 | `WANTS_FRAMES` | подписаться на frames ring (default — implicit) |
|
||||||
|
| 1 | `WANTS_PACKETS` | подписаться на packet ring |
|
||||||
|
| 2–31 | reserved | 0 |
|
||||||
|
|
||||||
|
Если subscriber оставляет flags=0 — publisher интерпретирует как `WANTS_FRAMES=true, WANTS_PACKETS=false`.
|
||||||
|
|
||||||
|
#### HELLO_RESP — packet-ring fields в reserved секции
|
||||||
|
|
||||||
|
```
|
||||||
|
[4 bytes] packet_shm_path_len (LE u32) 0 = packets disabled at publisher
|
||||||
|
[N bytes] packet_shm_path (UTF-8) e.g. "cuframes-cam1-packets" (relative to /dev/shm/)
|
||||||
|
[4 bytes] codec_id (LE u32) AV_CODEC_ID_*
|
||||||
|
[8 bytes] initial_packet_seq (LE u64) last_keyframe_seq на момент handshake
|
||||||
|
```
|
||||||
|
|
||||||
|
Если subscriber запросил `WANTS_PACKETS=1`, но publisher не имеет packet ring → `result = ERR_NOT_FOUND`.
|
||||||
|
|
||||||
|
## 11. Open для v0.5+
|
||||||
|
|
||||||
|
Эти решения **не** должны нарушить v4-совместимость:
|
||||||
|
|
||||||
|
- **FD wakeup channel** — возврат `EVENT_FD` message + eventfd для replace polling (latency win);
|
||||||
|
- **Multi-codec в одном publisher** — отдельный slot для thumbnail meta;
|
||||||
|
- **AMD/ROCm HIP IPC** — заменит cuMemCreate на rocSharedMemoryCreate;
|
||||||
|
- **Cross-host** через RDMA — отдельный transport, новый proto_version.
|
||||||
|
|
||||||
|
Любое из этих → bump `proto_version` в v5, отдельный документ.
|
||||||
|
|
||||||
|
## См. также
|
||||||
|
|
||||||
|
- [C API reference](/docs/reference/api-c) — обёртка над protocol.
|
||||||
|
- [Synchronization & VMM stream](/docs/concepts/sync-vmm-stream) — почему `cuStreamSynchronize`.
|
||||||
|
- [Frame ring vs Packet ring](/docs/concepts/frame-vs-packet-ring) — когда нужно использовать какой канал.
|
||||||
|
- [Install](/docs/getting-started/install) — runtime-требования.
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
---
|
||||||
|
title: cuframes — zero-copy GPU video frames across processes
|
||||||
|
hide_table_of_contents: true
|
||||||
|
---
|
||||||
|
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
|
|
||||||
|
# cuframes
|
||||||
|
|
||||||
|
**Zero-copy decoded video frames over CUDA, shared across processes — without pid namespace sharing.**
|
||||||
|
|
||||||
|
A small (~140 KB shared object) C library for Linux. Producer decodes once on the GPU; any number of consumer processes get the same frames via POSIX file descriptors and CUDA VMM. No re-encode, no host-side memcpy, no Unix-pipe serialization.
|
||||||
|
|
||||||
|
Built for video pipelines where the same decoded stream feeds several specialized processes (NVR record + AI inference + GPU compositor) and you don't want to run N decoders.
|
||||||
|
|
||||||
|
<div style={{display: 'flex', gap: '12px', marginTop: '24px', flexWrap: 'wrap'}}>
|
||||||
|
<Link className="button button--primary button--lg" to="/docs/intro">What is cuframes →</Link>
|
||||||
|
<Link className="button button--secondary button--lg" to="/docs/getting-started/install">Install</Link>
|
||||||
|
<Link className="button button--secondary button--lg" to="https://git.goldix.org/gx/cuframes">Source</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
## Quick example
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Publisher: 30 lines to share decoded NV12 frames
|
||||||
|
cuframes_publisher_config_t cfg = {
|
||||||
|
.key = "cam1",
|
||||||
|
.width = 1920, .height = 1080, .format = CUFRAMES_FORMAT_NV12,
|
||||||
|
.ownership = CUFRAMES_OWNERSHIP_LIBRARY,
|
||||||
|
.ring_size = 4,
|
||||||
|
};
|
||||||
|
cuframes_publisher_t *pub;
|
||||||
|
cuframes_publisher_create(&cfg, &pub);
|
||||||
|
|
||||||
|
while (alive) {
|
||||||
|
void *cuda_ptr;
|
||||||
|
cuframes_publisher_acquire(pub, &cuda_ptr);
|
||||||
|
decode_into(cuda_ptr, stream); // your decoder
|
||||||
|
cuframes_publisher_publish(pub, stream, cuframes_now_ns());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```c
|
||||||
|
// Subscriber: just connect by key, frames arrive
|
||||||
|
cuframes_subscriber_config_t cfg = {.key = "cam1", .consumer_name = "ai"};
|
||||||
|
cuframes_subscriber_t *sub;
|
||||||
|
cuframes_subscriber_create(&cfg, &sub);
|
||||||
|
|
||||||
|
cuframes_frame_t *f;
|
||||||
|
cuframes_subscriber_next(sub, my_stream, &f, /*timeout_ms*/ 1000);
|
||||||
|
void *cuda_ptr = cuframes_frame_cuda_ptr(f); // same GPU memory as publisher
|
||||||
|
process_with_cuda(cuda_ptr, my_stream);
|
||||||
|
cuframes_subscriber_release(sub, f);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
| | Without cuframes | With cuframes |
|
||||||
|
| --------------------------------- | ------------------------------------- | -------------------------- |
|
||||||
|
| 4 cameras × 3 consumers | 12 RTSP streams, 12 NVDEC decodes | 4 RTSP, 4 NVDEC |
|
||||||
|
| Cross-process shared GPU memory | CUDA IPC handles (needs PID share) | POSIX FD via `SCM_RIGHTS` |
|
||||||
|
| Frigate / s6-overlay containers | Broken — can't share PID | Works |
|
||||||
|
| Footprint | DeepStream multi-GB runtime | 140 KB `.so` + headers |
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
**v0.4 — early but production-tested.** Single deployment (4 IP cameras + NVR + grid compositor + AI detector) has been running on the maintainer's home server. Not enterprise-ready: one maintainer, no SLA, ABI broke four times during initial development.
|
||||||
|
|
||||||
|
If you're building your own pipeline and can read C — try it. If you need vendor support and an enterprise contract — use DeepStream.
|
||||||
|
|
||||||
|
[FAQ → Is cuframes production-ready?](/docs/faq#is-cuframes-production-ready)
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- **[Getting started](/docs/getting-started/install)** — install, first publisher, first subscriber (10 minutes)
|
||||||
|
- **[Concepts](/docs/concepts/frame-vs-packet-ring)** — frame vs packet ring, ownership modes, the v0.4 stream-sync model
|
||||||
|
- **[Integration](/docs/integration/ffmpeg-demuxer)** — FFmpeg `cuframes://` demuxer, `vf_cuda_grid` filter
|
||||||
|
- **[Reference](/docs/reference/api-c)** — full C / C++ API, wire protocol spec
|
||||||
|
- **[FAQ](/docs/faq)** — comparisons, license, common questions
|
||||||
|
|
||||||
|
LGPL-2.1+. [Source on Gitea](https://git.goldix.org/gx/cuframes).
|
||||||
Reference in New Issue
Block a user