Files
cuframes/docs/protocol.md
T
gx 6608f5d2f6 docs(protocol): bit-exact wire protocol specification (R4)
Closes последний RED-flag из arch review.

Что описано (§-sections):
1. Resources & lifecycle (socket / shm / IPC handles cleanup, crash recovery)
2. Shared memory byte-by-byte layout (offsets, packing, atomics)
   2.1 frame meta (64 bytes)
   2.2 slot descriptor (192 bytes)
   2.3 subscriber slot (128 bytes)
3. Unix socket TLV protocol (8 message types, framing)
4. State machines (subscriber-side, publisher-side per-subscriber)
5. ACK protocol с cudaEventRecord / cudaStreamWaitEvent
6. Versioning rules (proto_version vs lib_version, reserved fields)
7. Conformance test skeleton (offset checks, sizeof checks, handshake)
8. Open для v0.2 (TLS, multi-format, ROCm)
9. Reference impl pointer (libcuframes/src/protocol.c — Phase 1)

После v0.2 release — wire protocol frozen, breaking changes = bump
proto_version. До v0.2 — experimental.

Решает все 4 пункта из arch review section R4:
✓ SHM layout (annotated struct + ASCII layout)
✓ Socket protocol (state machine + message framing)
✓ Versioning rules
✓ Lifecycle / cleanup (incl. CUDA IPC handle leak при crash)

Готов к Step 2 (Phase 1 implementation).
2026-05-14 23:04:46 +01:00

426 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# cuframes Wire Protocol — version 1
**Статус:** v1 — experimental (subject to change до v0.2 release; после — semver-stable)
**Endianness:** little-endian (CUDA-platforms only)
**Дата:** 2026-05-15
Этот документ описывает **byte-exact** формат всех структур и сообщений
cuframes. Любая реализация (C, Python, Rust, FFmpeg filter) должна
соответствовать этому документу.
Связано с: [architecture.md](architecture.md) §2.2.
## 1. Resources / Lifecycle
Один publisher создаёт три kernel-уровневых ресурса:
| Resource | Path | Назначение | Cleanup |
|---|---|---|---|
| Unix socket | `/run/cuframes/<key>.sock` | Handshake + control plane | unlink при destroy() / orphaned после crash — cleanup'ится при next create через `O_EXCL` retry |
| Shared memory | `/dev/shm/cuframes-<key>` | Ring header + slot descriptors | shm_unlink при destroy() / orphaned автоматически если nobody mmap'ит |
| CUDA IPC handles | (no path, через handle in SHM) | Frame data в VRAM | освобождается когда last `cudaIpcCloseMemHandle` (consumer side) |
`<key>` — ASCII, [a-zA-Z0-9_-], 1-63 байт. Library валидирует regex `^[a-zA-Z0-9_-]{1,63}$`.
### Lifecycle при normal shutdown
```
1. cuframes_publisher_destroy() вызван.
2. Publisher шлёт всем connected subscribers: SHUTDOWN message в socket.
3. Subscribers закрывают handles (cudaIpcCloseMemHandle), socket, munmap, return DISCONNECTED.
4. Publisher: cudaIpcClose own pool, close socket, unlink socket+shm.
```
### Lifecycle при abnormal shutdown (publisher crash)
Producer не успевает unlink. Stale socket и shm остаются. Recovery:
```
Next 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 detect publisher crash через socket EOF (recv returns 0) или
SIGPIPE на send. После этого они должны cudaIpcCloseMemHandle на всех slot
pointers и return DISCONNECTED в `cuframes_subscriber_next`.
**CRITICAL**: CUDA IPC mem-handles **leak** в consumer'ах если publisher умер
без шанса всем разослать SHUTDOWN. Subscribers ОБЯЗАНЫ Close на DISCONNECTED.
## 2. Shared memory layout
`/dev/shm/cuframes-<key>` имеет фиксированный размер:
`sizeof(cuframes_shared_header_t)`. Без variable-length секций.
### Byte layout (all fields packed, no padding except where shown)
```
Offset Size Field Comments
─────── ────── ────────────────────── ─────────────────────────────────────
0x0000 4 magic (LE u32) 0xCC7C1DCC
0x0004 4 proto_version (LE u32) 1 (см. cuframes_protocol_version())
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, 1=EXTERNAL
0x0030 8 policy 0=DROP_OLDEST, 1=STRICT_WAIT
0x0038 8 max_subscribers 32 (захардкожено в v1)
0x0040 64 meta cuframes_frame_meta packed (см. §2.1)
0x0080 64 ipc_event_handle cudaIpcEventHandle_t (64 байта, opaque NVIDIA blob)
0x00C0 8 global_seq (LE u64) atomic, монотонная
0x00C8 8 subscriber_bitmap atomic, bit per subscriber slot
0x00D0 8 shutdown_flag atomic, 0=normal, 1=publisher 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)
- Максимальный size SHM: 0x0100 + 16×192 + 32×128 = ~7.3 KB. Headроom для v2 expansion.
Все 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 cuda_mem_handle cudaIpcMemHandle_t (NVIDIA opaque blob)
0x60 32 cuda_ptr_external для OWNERSHIP=EXTERNAL — original
user pointer (informative, not for
consumer use)
0x80 16 reserved_a
0x90 48 reserved_b
0xC0 END
```
Slot — статичный (не меняется после publisher create), за исключением
seq / pts_ns / ack_bitmap / written_bytes которые обновляются атомарно
на каждом publish.
### 2.3 Subscriber slot (128 байт)
В array индексирован bit-position в bitmap (0..31). Создаётся publisher'ом
во время handshake, освобождается на unsubscribe / dead-man timeout.
```
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 — последний seq которое
subscriber успел ACK
0x18 8 last_ack_ns (LE i64) wall-clock последнего ACK
(для consumer_ack_timeout_ms)
0x20 32 consumer_name ASCII zero-terminated, max 31 char
0x40 64 reserved
```
Bit-position **0** зарезервирован — никогда не assigned (sentinel). Используется
bits 1..31 → max 31 subscribers (см. errcode TOO_MANY).
## 3. Unix socket protocol
Publisher `listen()`'ит SOCK_STREAM по `/run/cuframes/<key>.sock`.
Subscriber `connect()`'ится.
Сокет используется для:
- Handshake (subscribe / negotiate)
- Lifetime signals (SHUTDOWN, force-disconnect)
- Late-joining subscribers получают current slot snapshot
Сокет **не** используется для frame transfer — это shared memory + atomic
`global_seq` + (Phase 1+) eventfd для wakeup polling consumers.
### 3.1 Framing
Каждое сообщение — TLV (type-length-value):
```
[4 bytes] message_type (LE u32, see §3.2)
[4 bytes] payload_length (LE u32, bytes excl. these 8 header bytes)
[N bytes] payload (длина = payload_length)
```
`payload_length` max = 4096 для v1 (без длинных сообщений).
### 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 |
| 0x10 | `UNSUBSCRIBE` | C→P | Graceful disconnect |
| 0x20 | `EVENT_FD` | P→C | (Phase 1+) FD wake-up channel (sendmsg + cmsg) |
| 0x30 | `SHUTDOWN` | P→C | Publisher shutting down |
| 0xF0 | `PING` | both | Liveness check |
| 0xF1 | `PONG` | both | Reply to PING |
| 0xFE | `ERROR` | both | Error notification (см. §3.3) |
### 3.3 HELLO_REQ payload (consumer → publisher)
```
[4 bytes] proto_version (LE u32) wire-protocol version consumer'а
[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'а
[4 bytes] ring_size (LE u32)
[4 bytes] ownership_mode (LE u32)
[64 bytes] frame_meta (см. §2.1)
[4 bytes] shm_path_len (LE u32)
[N bytes] shm_path (UTF-8, ASCII подмножество)
[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
[4 bytes] initial_seq (LE u64) — текущий global_seq на момент handshake;
consumer старт reading с этой точки
[12 bytes] reserved
```
После успешного SUBSCRIBE_RESP — handshake complete. Consumer может poll'ить
`global_seq` в SHM и читать frames.
### 3.7 EVENT_FD (Phase 1+)
Publisher отправляет file descriptor (eventfd) через `sendmsg()` + ancillary
data (`SCM_RIGHTS`). Payload содержит только metadata:
```
[4 bytes] fd_purpose (LE u32) 1 = wake-up на новый seq
[12 bytes] reserved
```
Сам FD приходит в `cmsg`. Consumer `read()`'ит eventfd — блокируется до
producer's `eventfd_write(1)` на следующем publish.
В v1 PoC может быть omitted — consumers poll'ят `global_seq` с usleep.
EVENT_FD добавляется когда poll становится bottleneck'ом (latency).
### 3.8 SHUTDOWN payload
```
[4 bytes] reason (LE u32) 0 = normal, 1 = error, 2 = upgrade
[12 bytes] reserved
```
Subscriber получивший SHUTDOWN → cuda_ipc_close на всех slot pointers,
return 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. State machines
### 4.1 Subscriber-side
```
┌──────────┐ socket connect()
│ STARTUP │────────────────────┐
└──────────┘ ▼
┌─────────┐
│ HELLO │ send HELLO_REQ
└────┬────┘
│ recv HELLO_RESP (ok)
┌─────────┐
│SUBSCRIBE│ send SUBSCRIBE_REQ
└────┬────┘
│ recv SUBSCRIBE_RESP (ok, bit assigned)
┌─────────┐ cuda_ipc_open events / mem
│ READY │
└────┬────┘
socket EOF │ user calls destroy
или SHUTDOWN│ │
▼ ▼
┌─────────────┐
│ CLEANUP │ cuda_ipc_close, unsubscribe send
└─────────────┘
```
### 4.2 Publisher-side per-subscriber
```
recv HELLO_REQ
validate (proto_version, format)
│ │
ok fail
│ └─→ send HELLO_RESP(result=PROTOCOL/FORMAT) → close socket
▼ send HELLO_RESP(ok), wait SUBSCRIBE_REQ
┌─────────────┐
│ HANDSHAKING │
└──────┬──────┘
│ recv SUBSCRIBE_REQ
allocate subscriber_slot, assigned_bit
ok / TOO_MANY?
▼ send SUBSCRIBE_RESP(ok, bit, initial_seq)
┌──────────┐
│ ACTIVE │ — publisher includes этот bit в bitmap requirement
└──────────┘ (для STRICT_WAIT)
subscriber sends UNSUBSCRIBE,
socket EOF, ИЛИ
liveness timeout (PID dead)
release subscriber_slot, clear bit in bitmap
```
## 5. ACK protocol
При публикации slot N publisher:
1. Записывает frame data (или регистрирует external pointer)
2. `cudaEventRecord(event, stream)` на producer stream
3. Atomic: `slot[N].seq = next_seq`, `slot[N].pts_ns = now`, `slot[N].ack_bitmap = 0`
4. Atomic RELEASE: `global_seq = next_seq`
5. (Optional) `eventfd_write(1)` для каждого подписчика
Subscriber:
1. ACQUIRE load `global_seq`. Если новое — process.
2. `cudaStreamWaitEvent(consumer_stream, producer_event, 0)`
3. Process frame (CUDA kernel, copy, etc.)
4. ACK: `atomic_fetch_or(&slot.ack_bitmap, 1 << my_bit, RELEASE)`
5. Atomic store `last_seen_seq = seq`, `last_ack_ns = now`
Publisher (STRICT_WAIT mode) перед next publish ждёт:
- `ack_bitmap == subscriber_bitmap` (все активные ACK'нули), ИЛИ
- timeout `consumer_ack_timeout_ms` истёк → mark dead subscriber, clear bit
Publisher (DROP_OLDEST mode) — не ждёт ACK, просто перезаписывает slot.
## 6. Versioning rules
### 6.1 Wire protocol version
`proto_version` — single integer. v1 — текущий. Breaking changes → bump.
Handshake:
- Если subscriber.proto_version < publisher.proto_version: publisher может
ответить HELLO_RESP с downgraded format (если backward-compat).
v1 — нет backward layer, mismatch → ERROR(PROTOCOL).
- Если subscriber > publisher: publisher отвечает ERROR(PROTOCOL).
### 6.2 Library version (semver)
`lib_version_major.minor.patch` — informational, не используется для
compat decisions. Передаётся в SHM header для diagnostics.
### 6.3 Reserved fields
Все `reserved_*` — должны быть **0** при write. Reader игнорирует.
Это позволяет в minor releases добавлять fields в reserved space без
breaking ABI (т.к. struct sizes остаются те же).
## 7. Conformance test skeleton
Тесты в `tests/conformance/` (Phase 1):
```c
// test_layout.c
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, ipc_event_handle), 0x0080);
// ... see §2 table
}
TEST(ProtocolLayout, SlotDescriptorSize) {
EXPECT_EQ(sizeof(cuframes_slot_descriptor_t), 192);
}
TEST(ProtocolLayout, SubscriberSlotSize) {
EXPECT_EQ(sizeof(cuframes_subscriber_slot_t), 128);
}
```
```c
// test_handshake.c
TEST(Handshake, HelloRespMismatchProto) {
// Setup publisher with proto_version=1
// Connect subscriber with proto_version=2
// Expect: HELLO_RESP(result=ERR_PROTOCOL)
}
```
## 8. Open для v0.2
Эти решения **не** должны нарушить v1 compat:
- TLS / authentication для cross-host scenarios (если когда-нибудь)
- Multi-format в одном publisher (отдельный slot для thumbnail meta)
- AMD/ROCm IPC (заменит cuda_ipc_event_handle на разноименный)
Любое из этих → bump `proto_version` в v2, отдельный document.
## 9. Reference implementation
`libcuframes/src/protocol.c` (Phase 1, Step 2) — единственная reference.
Любая другая реализация (Python ctypes, Rust bindings, FFmpeg plugin)
должна **conformance-tested** против этого документа.