diff --git a/docs/protocol.md b/docs/protocol.md index 8b30783..a344a00 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -423,3 +423,400 @@ TEST(Handshake, HelloRespMismatchProto) { `libcuframes/src/protocol.c` (Phase 1, Step 2) — единственная reference. Любая другая реализация (Python ctypes, Rust bindings, FFmpeg plugin) должна **conformance-tested** против этого документа. + +## 10. v0.2 extension: encoded packet ring (proto_version=2) + +**Статус:** design draft, ещё не реализовано (см. issue #2). + +Параллельно с decoded-frames ring (§2) publisher может опционально +поддерживать **encoded packet ring** — публикует raw H.264/H.265 NAL units +**до** decoder, для consumer'ов которые делают `-c:v copy` (recording, mux). + +### 10.1 Совместимость с v1 + +- v2 publisher принимает **v1-subscribers** — они получают только frames + ring (как v0.1), packet ring им не показывается. +- v1 publisher отвергает v2-subscribers с `wants_packets=true` + (HELLO_RESP error PROTOCOL). +- v1 layout (§2) **не меняется** для frames ring — packet ring это отдельный SHM. + +Publisher version bumping: +- `proto_version` = 2 в SHM header и в HELLO_RESP когда packet ring active. +- Если publisher v2 не активирует packet ring (`enable_packet_ring=false`) + — `proto_version` остаётся 1 (полная v1 compat). + +### 10.2 Дополнительные ресурсы + +| Resource | Path | Назначение | Когда | +|---|---|---|---| +| Packet shared memory | `/dev/shm/cuframes--packets` | Packet ring header + slots + byte buffer | если publisher активировал packet ring | + +Cleanup — симметрично §1: `shm_unlink` при destroy(); orphaned автоматически +если nobody mmap'ит. + +### 10.3 Packet ring layout + +Размер пакетного SHM: `sizeof(packet_ring_header_t) + N×PSE + DATA_SIZE`, +где: +- N = `packet_ring_slots`, default 64 (configurable) +- PSE = `sizeof(packet_slot_entry_t)` = 64 байт (см. §10.5) +- DATA_SIZE = `packet_data_size`, default 8 MB (configurable) + +#### Byte layout + +``` +Offset Size Field Comments +─────────────────────── ────── ────────────────────────── ───────────────────────────── +0x0000 4 magic (LE u32) 0xCC7C1DCD (frames magic + 1) +0x0004 4 proto_version (LE u32) 2 +0x0008 4 ring_slots (LE u32) N (1..1024) +0x000C 4 data_size (LE u32) bytes for packet data ring +0x0010 4 codec_id (LE u32) AV_CODEC_ID_* enum (см. §10.4) +0x0014 4 codec_extradata_size (LE u32) ≤ 4096 +0x0018 8 producer_pid (LE u64) +0x0020 8 global_seq (LE u64, atomic) монотонная по packets +0x0028 8 last_keyframe_seq (LE u64, atomic) для late subscribers +0x0030 8 write_offset (LE u64, atomic) текущий cursor в data ring +0x0038 8 shutdown_flag (LE u64, atomic) +0x0040 4096 codec_extradata SPS/PPS/VPS bytes (см. §10.4) +0x1040 N×64 slots[N] packet_slot_entry_t (см. §10.5) +0x1040+N×64 DATA_SIZE data[] wraparound byte buffer +``` + +Все atomic fields — C11 `_Atomic` (release/acquire semantics для seq updates). + +### 10.4 Codec extradata + +H.264 — SPS + PPS, конкатенированные в **Annex B** формате +(start codes `00 00 00 01`). H.265 — VPS + SPS + PPS. + +`codec_id` соответствует FFmpeg `AV_CODEC_ID_H264`, `AV_CODEC_ID_HEVC`, +`AV_CODEC_ID_AV1` (future). Subscriber пишет этот extradata в +`AVCodecContext.extradata` своего decoder'а (если он его создаёт) +или в `AVStream.codecpar->extradata` для muxer'ов. + +Extradata устанавливается publisher'ом **один раз** при первом keyframe +(или из RTSP SDP до первого packet). После — fixed на lifetime publisher'а +(codec change mid-stream → publisher destroy+recreate с новым ``). + +### 10.5 Packet slot entry (64 байта) + +``` +Offset Size Field Comments +0x00 8 seq (LE u64, atomic) published seq; UINT64_MAX = invalid +0x08 8 pts_ns (LE i64) +0x10 8 dts_ns (LE i64) для B-frames pipelines +0x18 8 data_offset (LE u64) offset в `data[]` секции SHM +0x20 4 data_size (LE u32) size of payload bytes +0x24 4 flags (LE u32) §10.6 +0x28 24 reserved 0 +``` + +`data_offset` может быть **больше** `data_size` секции SHM — semantics +"absolute byte cursor", фактический byte index = `data_offset % data_size`. +Subscriber может detect wrap (если payload crosses end → split read). + +### 10.6 Packet flags + +``` +Bit Name Comments +0 KEY keyframe (IDR for H.264, или CRA/IDR для HEVC). + Critical для late subscribers — must wait IDR. +1 CORRUPT publisher detect'нул что packet damaged + (RTP loss и т.п.) — subscriber может skip +2 DISCONTINUITY был gap перед этим packet + (publisher reconnect к камере) +3 LAST_IN_AU last NAL в access unit (полный frame) + — для muxer'ов которые ждут полный frame +4-31 reserved 0 +``` + +Mapping в `AVPacket.flags`: +- bit 0 (KEY) → `AV_PKT_FLAG_KEY` +- bit 1 (CORRUPT) → `AV_PKT_FLAG_CORRUPT` +- bit 2 (DISCONTINUITY) → `AV_PKT_FLAG_DISCONTINUITY` (FFmpeg 5+) + +### 10.7 Atomic publish (publisher-side) + +```c +// Pseudo-C (упрощено, без error handling) +uint64_t seq = atomic_load(&hdr->global_seq, RELAXED) + 1; +uint64_t off = atomic_load(&hdr->write_offset, RELAXED); + +// 1. Найти free slot (overwrite oldest) +size_t slot_idx = seq % hdr->ring_slots; +packet_slot_entry_t *slot = &slots[slot_idx]; + +// 2. Записать payload bytes (wraparound, может потребовать 2 memcpy) +size_t off_in_ring = off % hdr->data_size; +size_t first_chunk = min(size, hdr->data_size - off_in_ring); +memcpy(data + off_in_ring, payload, first_chunk); +if (first_chunk < size) + memcpy(data, payload + first_chunk, size - first_chunk); + +// 3. RELEASE: записать metadata в slot +slot->pts_ns = pts; +slot->dts_ns = dts; +slot->data_offset = off; +slot->data_size = size; +slot->flags = flags; +atomic_store(&slot->seq, seq, RELEASE); + +// 4. Update global cursor + global_seq +atomic_store(&hdr->write_offset, off + size, RELEASE); +atomic_store(&hdr->global_seq, seq, RELEASE); + +// 5. If KEY → update last_keyframe_seq +if (flags & PKT_FLAG_KEY) + atomic_store(&hdr->last_keyframe_seq, seq, RELEASE); +``` + +### 10.8 Atomic read (subscriber-side) + +```c +// Pseudo-C +uint64_t cur = atomic_load(&hdr->global_seq, ACQUIRE); +if (cur <= my_last_seq) return TIMEOUT; // ничего нового + +uint64_t want_seq = my_last_seq + 1; +size_t slot_idx = want_seq % hdr->ring_slots; +packet_slot_entry_t *slot = &slots[slot_idx]; + +uint64_t slot_seq = atomic_load(&slot->seq, ACQUIRE); +if (slot_seq != want_seq) { + // overrun — slow subscriber. Re-anchor: + want_seq = atomic_load(&hdr->last_keyframe_seq, ACQUIRE); + slot_idx = want_seq % hdr->ring_slots; + slot = &slots[slot_idx]; + return DROPPED; // signal user через flags = DISCONTINUITY +} + +// Copy payload (wraparound aware) +uint64_t off = slot->data_offset % hdr->data_size; +uint32_t size = slot->data_size; +uint32_t first_chunk = min(size, hdr->data_size - off); +memcpy(out_buf, data + off, first_chunk); +if (first_chunk < size) + memcpy(out_buf + first_chunk, data, size - first_chunk); + +// Re-check slot->seq не изменился (защита от overrun mid-read) +if (atomic_load(&slot->seq, ACQUIRE) != want_seq) { + return DROPPED; // publisher overwrote во время copy +} + +my_last_seq = want_seq; +return OK; +``` + +Защита от overrun mid-read через **post-check `slot->seq`** — простая +вариант seqlock. Если publisher успел overwrite между metadata-read и +data-copy — subscriber detect и retry. + +### 10.9 Socket protocol extensions + +#### HELLO_REQ — добавляются flags в reserved field + +v1 layout (§3.3): +``` +[4 bytes] proto_version +[4 bytes] consumer_name_len +[N bytes] consumer_name +[4 bytes] cuda_device +[4 bytes] mode +[12 bytes] reserved (must be 0) ← v0.2 использует первые 4 байта +``` + +v0.2 интерпретирует первые 4 байта `reserved` как `subscribe_flags`: + +| Bit | Name | Comments | +|---|---|---| +| 0 | `WANTS_FRAMES` | подписаться на decoded frames ring (default ON в v1 — implicit) | +| 1 | `WANTS_PACKETS` | подписаться на encoded packet ring | +| 2-31 | reserved | 0 | + +Если v1-subscriber оставляет reserved=0 — publisher v2 интерпретирует это +как `WANTS_FRAMES=true, WANTS_PACKETS=false` (v1 backward-compat). + +#### HELLO_RESP — добавляются packet-ring fields + +v1 layout (§3.4) расширяется в reserved секции: + +``` +[4 bytes] result +[4 bytes] proto_version_actual ← теперь может быть 1 или 2 +[4 bytes] ring_size ← frames ring +[4 bytes] ownership_mode +[64 bytes] frame_meta +[4 bytes] shm_path_len ← frames SHM +[N bytes] shm_path +[12 bytes] reserved ← v0.2 интерпретирует +``` + +v0.2 reserved layout (если `proto_version_actual == 2` И publisher +поддерживает packets): +``` +[4 bytes] packet_shm_path_len (LE u32) 0 = packets disabled at publisher +[N bytes] packet_shm_path (UTF-8) — относительно /dev/shm/, например "cuframes-camA-packets" +[4 bytes] codec_id (LE u32) AV_CODEC_ID_* +[4 bytes] initial_packet_seq (LE u64) last_keyframe_seq на момент handshake + (subscriber должен start с этого seq) +``` + +Если subscriber запросил `WANTS_PACKETS=1` но publisher не имеет packet ring +— `result = ERR_NOT_AVAILABLE`. + +### 10.10 Subscriber state machine extension + +Подключение к **обоим** rings (или одному из): + +``` + ┌──────────┐ + │ HELLO_OK │ proto_version_actual=2, packet_shm_path_len>0 + └────┬─────┘ + │ + ▼ + ┌────────────────────────────────┐ + │ Open frames SHM (если WANTS_FRAMES) │ → standard v1 flow + └────────────────────────────────┘ + │ + ▼ + ┌────────────────────────────────┐ + │ Open packet SHM (если WANTS_PACKETS) │ + │ - mmap /dev/shm/cuframes--packets │ + │ - check magic, proto_version │ + │ - set my_last_packet_seq = initial_packet_seq - 1 │ + │ (так что первый next_packet вернёт IDR) │ + └────────────────────────────────┘ + │ + ▼ + ┌─────────┐ + │ READY │ — frames или packets или оба доступны + └─────────┘ +``` + +### 10.11 Threading в subscriber + +Frames ring и packet ring имеют **разные** `global_seq` counters. +Subscriber имеет **отдельные** `my_last_seq` для каждого. Может +poll'ить обе независимо (или через два threads). + +Producer's `cudaEventRecord` (frames sync) не релевантен для packets — +encoded data на CPU, без CUDA sync. + +### 10.12 Конфигурируемость packet ring + +Publisher API extension (§10.13) принимает параметры: + +```c +typedef struct { + uint32_t packet_ring_slots; // default 64 + uint32_t packet_data_size; // default 8 MB (8388608) + uint32_t max_packet_size; // default 2 MB — sanity guard для оversized + // packets (publisher rejects with error) + uint32_t codec_id; // AV_CODEC_ID_H264 / HEVC / ... +} cuframes_packet_ring_options_t; +``` + +### 10.13 API extension (для cuframes.h) + +```c +/* Сreate publisher с активным packet ring. NULL для opts → packet ring disabled. */ +int cuframes_publisher_create_ex( + const cuframes_publisher_options_t *frames_opts, + const cuframes_packet_ring_options_t *packet_opts, /* NULL = no packet ring */ + cuframes_publisher_t **pub_out +); + +/* Set codec extradata (SPS/PPS) — должен быть called до первого publish_packet. */ +int cuframes_publisher_set_codec_extradata( + cuframes_publisher_t *pub, + const void *extradata, + size_t size +); + +/* Публикация packet. Slow consumer = overwrite oldest. */ +int cuframes_publisher_publish_packet( + cuframes_publisher_t *pub, + const void *data, + size_t size, + int64_t pts_ns, + int64_t dts_ns, + uint32_t flags /* CUFRAMES_PKT_FLAG_KEY | _CORRUPT | _DISCONTINUITY | _LAST_IN_AU */ +); + +/* Subscriber-side: подписаться с opt-in для packets. */ +typedef struct { + /* ... existing v1 fields ... */ + uint32_t subscribe_flags; /* WANTS_FRAMES, WANTS_PACKETS bits */ +} cuframes_subscriber_options_v2_t; + +int cuframes_subscriber_create_v2( + const cuframes_subscriber_options_v2_t *opts, + cuframes_subscriber_t **sub_out +); + +/* Чтение packet. Opaque handle — каллер вызывает release_packet после. */ +typedef struct cuframes_packet cuframes_packet_t; + +int cuframes_subscriber_next_packet( + cuframes_subscriber_t *sub, + cuframes_packet_t **pkt_out, + int32_t timeout_ms +); + +const void * cuframes_packet_data(const cuframes_packet_t *p); +size_t cuframes_packet_size(const cuframes_packet_t *p); +int64_t cuframes_packet_pts(const cuframes_packet_t *p); +int64_t cuframes_packet_dts(const cuframes_packet_t *p); +uint32_t cuframes_packet_flags(const cuframes_packet_t *p); + +int cuframes_subscriber_release_packet(cuframes_subscriber_t *sub, cuframes_packet_t *p); + +/* Codec params для subscriber (extracted из shared header). */ +int cuframes_subscriber_get_codec_params( + cuframes_subscriber_t *sub, + uint32_t *codec_id_out, + const void **extradata_out, + size_t *extradata_size_out +); +``` + +`cuframes_packet_t` opaque — фактически указатель в local-mapped data (на +heap subscriber'а — copy при `next_packet`, освобождение при `release`). +Subscriber **не** держит ссылки на shared ring data между `next_packet` и +`release_packet` — это избавляет от reader-locks. + +### 10.14 Late subscriber → keyframe-aligned start + +При SUBSCRIBE_RESP publisher отвечает `initial_packet_seq = last_keyframe_seq`. + +Subscriber устанавливает `my_last_seq = initial_packet_seq - 1`, так что +первый `next_packet` вернёт keyframe (decoder может start без glitches). + +**Risk:** если в момент handshake **last_keyframe_seq уже выехал из +ring** (slow start subscriber, GOP > ring_slots packets) — subscriber +detect overrun в первом read и переходит на следующий keyframe. + +В implementation `publisher_publish_packet` для оптимизации может маркировать +slot перед IDR как **persistent** (флаг в reserved), но **v0.2 keep simple** — +просто требуем что `packet_ring_slots × avg_packet_size > GOP_size_in_bytes` +для нормальной работы. Sizing guide см. в [docs/integration.md](integration.md). + +### 10.15 Error codes (новые) + +| Code | Name | Когда | +|---|---|---| +| -20 | `CUFRAMES_ERR_PACKET_OVERSIZED` | publish_packet с size > max_packet_size | +| -21 | `CUFRAMES_ERR_NO_PACKET_RING` | subscriber запросил packets, publisher без packet ring | +| -22 | `CUFRAMES_ERR_NO_CODEC_PARAMS` | get_codec_params вызван до set_codec_extradata publisher'ом | +| -23 | `CUFRAMES_ERR_PACKET_OVERRUN` | subscriber slow — packet seq уехал, надо resync на keyframe | + +### 10.16 Open для v0.3+ + +- **Sub-stream selection** — publisher может публиковать несколько + packet rings (для multi-resolution streams). Сейчас один key = один stream. + v0.3 → `-substream-` naming? +- **Codec change mid-stream** — текущий design требует publisher restart. + Future: invalidate codec_extradata + bump generation field. +- **Audio streams** — analogichno в packet ring, но codec_id = audio (AAC, + Opus). v0.3.