Files
cuframes-docs/site/i18n/ru/docusaurus-plugin-content-docs/current/reference/protocol.md
T
Claude Opus 8c3c43709d docs: full content + landing + RU translations
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>
2026-05-26 23:31:03 +01:00

29 KiB
Raw Blame History

sidebar_position, title
sidebar_position title
3 Спецификация 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.

Аспект v1v3 (legacy) v4 (current)
Magic 0xCC7C1DCC 0xCC7C1DCE
Protocol version 13 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 HELLOSUBSCRIBE HELLOSUBSCRIBEVMM_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 — почему именно 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_-], 163 байт. 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 v1v3.

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 в v1v3)
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 в v1v3)
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 v1v3) — 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. cuMemAddressReservecuMemMapcuMemSetAccess (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

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 v1v3 не имеет шага 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'а:

// 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.

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_versionHELLO_RESP(result=ERR_PROTOCOL). В v4 нет backward-compat layer'а с v1v3 — 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/):

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
431 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)

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)

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
231 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, отдельный документ.

См. также