docs(protocol): v0.2 — encoded packet ring spec (§10)
build / cmake build (CUDA 12.4, Ubuntu 22.04) (pull_request) Successful in 1m35s
build / ffmpeg filter patch (out-of-tree) (pull_request) Successful in 1m39s

Полный wire-protocol spec для encoded packet ring:
- Отдельный SHM /dev/shm/cuframes-<key>-packets (variable-length)
- Backward-compat с v1: proto_version=2 publishers принимают v1 subscribers
- HELLO_REQ/HELLO_RESP extension через reserved bytes — без слома v1 layout
- Codec extradata (SPS/PPS) в shared header
- Late subscriber → keyframe-aligned start (initial_packet_seq)
- Seqlock pattern для защиты от overrun mid-read
- API extension: publish_packet, next_packet, get_codec_params
- 4 новых error codes (OVERSIZED, NO_PACKET_RING, NO_CODEC_PARAMS, PACKET_OVERRUN)

Связано: #2
This commit is contained in:
2026-05-19 16:04:00 +01:00
parent 264b9d59db
commit ad75aa9624
+397
View File
@@ -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-<key>-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 с новым `<key>`).
### 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-<key>-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 → `<key>-substream-<N>` 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.