diff --git a/site/docs/getting-started/first-publisher.md b/site/docs/getting-started/first-publisher.md index 31247c7..9467702 100644 --- a/site/docs/getting-started/first-publisher.md +++ b/site/docs/getting-started/first-publisher.md @@ -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. -**`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). diff --git a/site/docs/getting-started/first-subscriber.md b/site/docs/getting-started/first-subscriber.md index 3aa0eea..9e76a53 100644 --- a/site/docs/getting-started/first-subscriber.md +++ b/site/docs/getting-started/first-subscriber.md @@ -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)). diff --git a/site/docs/getting-started/install.md b/site/docs/getting-started/install.md index 787987d..74e4930 100644 --- a/site/docs/getting-started/install.md +++ b/site/docs/getting-started/install.md @@ -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. -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) @@ -41,7 +41,7 @@ docker run --rm --runtime=nvidia \ /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 diff --git a/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/first-publisher.md b/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/first-publisher.md index 5ea2eff..5ef5de7 100644 --- a/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/first-publisher.md +++ b/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/first-publisher.md @@ -75,7 +75,7 @@ int main(int argc, char **argv) { **`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 в наносекундах). diff --git a/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/first-subscriber.md b/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/first-subscriber.md index 4ad8358..58eb01f 100644 --- a/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/first-subscriber.md +++ b/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/first-subscriber.md @@ -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)). diff --git a/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/install.md b/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/install.md index 64a4996..c1846df 100644 --- a/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/install.md +++ b/site/i18n/ru/docusaurus-plugin-content-docs/current/getting-started/install.md @@ -7,7 +7,7 @@ sidebar_position: 1 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-образ (рекомендуется для первого знакомства) @@ -41,7 +41,7 @@ docker run --rm --runtime=nvidia \ /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 — сборка из исходников diff --git a/site/i18n/ru/docusaurus-plugin-content-docs/current/reference/api-c.md b/site/i18n/ru/docusaurus-plugin-content-docs/current/reference/api-c.md new file mode 100644 index 0000000..e5f80bc --- /dev/null +++ b/site/i18n/ru/docusaurus-plugin-content-docs/current/reference/api-c.md @@ -0,0 +1,531 @@ +--- +sidebar_position: 1 +title: C API +--- + +# C API reference + +Полный листинг public C API из `` (libcuframes 0.4.0). Source of truth — header в repo, эта страница его дублирует в Docusaurus-формате с cross-links на концептуальные разделы. + +## Headers & linkage + +```c +#include +``` + +```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/.sock`, mmap'ит `/dev/shm/cuframes-`. См. [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--`. 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--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-пример. diff --git a/site/i18n/ru/docusaurus-plugin-content-docs/current/reference/api-cpp.md b/site/i18n/ru/docusaurus-plugin-content-docs/current/reference/api-cpp.md new file mode 100644 index 0000000..027dc6d --- /dev/null +++ b/site/i18n/ru/docusaurus-plugin-content-docs/current/reference/api-cpp.md @@ -0,0 +1,351 @@ +--- +sidebar_position: 2 +title: C++ API +--- + +# C++ API reference + +`` — header-only RAII-wrapper над [C API](/docs/reference/api-c). Тонкий слой: handle-классы с automatic cleanup, exceptions вместо int return codes, `std::optional` для `next`. + +## Headers & linkage + +```cpp +#include +``` + +```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()` — `": "`. + +```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 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; + using OnError = std::function; + + 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-функции из ``, см. [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 +#include + +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 +#include + +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 +#include +#include + +int main() { + std::atomic 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 пример. diff --git a/site/i18n/ru/docusaurus-plugin-content-docs/current/reference/protocol.md b/site/i18n/ru/docusaurus-plugin-content-docs/current/reference/protocol.md new file mode 100644 index 0000000..1c4de65 --- /dev/null +++ b/site/i18n/ru/docusaurus-plugin-content-docs/current/reference/protocol.md @@ -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/.sock` | Handshake + control plane | unlink при `destroy()`; orphaned после crash — cleanup'ится при следующем `create` через `O_EXCL` retry | +| Frame SHM | `/dev/shm/cuframes-` | 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--packets` | Packet ring header + slots + data section | `shm_unlink` при `destroy()`; opt-in (только если вызван `enable_packets`) | + +`` — 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-`: если 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-` имеет фиксированный размер: `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/.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
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--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 с новым ``). + +### 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-требования. diff --git a/site/src/pages/index.mdx b/site/src/pages/index.mdx new file mode 100644 index 0000000..aebfd73 --- /dev/null +++ b/site/src/pages/index.mdx @@ -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. + +
+ What is cuframes → + Install + Source +
+ +## 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).