8c3c43709d
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>
609 lines
29 KiB
Markdown
609 lines
29 KiB
Markdown
---
|
||
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-требования.
|