Files
oak-editor/crates/oakengine/src/ipc.rs
T
Mike-Solar ab1a2e9c7b refactor: drop internal bridge/ffi layers; exporter family lands
Single-lib cleanup: the per-crate src/bridge/ and src/ffi.rs layers are
gone (oakundo/oakcommon/oaknode/oaktimeline/oakcodec/oakaudio/
oakrender/oaktask/oakplugin/oakstorage); cross-crate calls are plain
Rust, CHandle marshalling shrinks to the oakengine boundary, and tests
call the Rust APIs directly (pure C-ABI wrapper tests removed where
the domain layer already covers the behavior).

exporter.h family implemented: oakengine_export_render (CLI contract),
oakengine_export_render_with_params (was a stub), last_error and
progress callback; synchronous path reuses task_create_export +
start_sync. Fixes on the way: oaktask video ticket self-deadlock,
audio params dropped on the export path, codec encoder AAC slicing and
H.264 time base. Real-mp4 tests cover both entry points, progress and
the illegal-argument matrix.

Also: oakstorage session maps null project handles to None (version-
info path), configstore test double literal 3.14 -> 3.15 (clippy PI
lint), oakaudio output callback scratch buffer + env-aware P1 test,
cli media round-trip test uses a generated 16-frame clip (no more
minute-long debug runs).
2026-08-16 00:33:45 +08:00

2433 lines
77 KiB
Rust

// Oak Video Editor - Non-Linear Video Editor
// Copyright (C) 2026 Oak Team
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//! Render-worker IPC: the control-plane NDJSON protocol and the
//! shared-memory frame-slot transport, owned by the oakengine facade and
//! exported through the frozen `oakengine_ipc_*` C ABI
//! (`engine/include/oakengine/ipc.h`); the `oak-worker` binary consumes it
//! purely through the C ABI. The transport is the Rust port of
//! `engine/render/ipc/` + `ipcmessage.cpp`.
//!
//! Two halves:
//!
//! - **Control plane.** One compact JSON object per line on the stdio
//! pipes (worker.cpp / ipcmessage.cpp `write_message`/`read_message`).
//! Every message carries a `"type"` string; the field names below are
//! the ones the C++ serializers actually emit
//! (`engine/render/ipc/ipcmessage.cpp`): note `ticket` / `node` /
//! `channels` / `slot` — the longer names (`ticket_id`, `node_uuid`,
//! `channel_count`, `output_slot`) exist only on the C POD structs in
//! `ipc.h`. [`write_message`]/[`error_message`] build the wire lines.
//! - **Data plane.** Named shared memory holding the frame-slot pools —
//! the port of `engine/render/ipc/` (`sharedmemoryregion.cpp`,
//! `frameslotpool.cpp`): [`SharedMemoryRegion`] maps a named POSIX
//! segment (`shm_open` + `mmap`, `munmap` + `shm_unlink` on close),
//! and [`FrameSlotPool`] lays out a fixed pool of equal-sized frame
//! slots inside it with lock-free hand-off through two
//! [`SpscRingBuffer`]s of slot indices (free + ready). Each ring is a
//! single-producer/single-consumer structure; the filler owns
//! `free.pop` + `ready.push`, the drainer owns `ready.pop` +
//! `free.push`, so no mutex is ever taken.
//!
//! **The in-memory layout is the version-1 wire protocol** the app and the
//! render worker share, and it never changes: the byte offsets below are
//! copied field-for-field from the C++ implementation (64-byte cache-line
//! alignment, the `Header`/`SpscRingBuffer`/`oak_frame_slot_meta` POD
//! structs). A segment written by the C++ side attaches here and vice
//! versa.
//!
//! This module is deliberately unsafe-heavy and self-contained: it touches
//! raw shared memory and raw POSIX syscalls, and everything else in the
//! crate reaches it through the safe wrapper methods.
//!
//! Message types (M = main/editor, W = worker):
//! handshake M<->W negotiate protocol version + announce shm geometry
//! load_graph M ->W path to a temp file holding the serialized graph
//! render_frame M ->W request a frame render (ticket, node, time, params)
//! frame_ready W ->M a rendered frame is published (slot + ticket)
//! cancel M ->W abandon an in-flight ticket
//! graph_update M ->W reserved (no payload struct yet)
//! shutdown M ->W finish current work and exit cleanly
//! error W ->M worker-side failure report ("message" field)
//!
//! Items the worker does not emit yet (frame_ready, graph_update,
//! `FrameReadyMsg`) and message ids it ignores (`cancel`) are kept as the
//! documented protocol surface; `dead_code` until the frame-slot transport
//! is driven by a real graph (see [`crate::worker`]).
#![allow(dead_code)]
use std::ffi::{c_char, c_int, c_void};
use std::io::{self, Write};
use std::ptr;
use std::sync::atomic::{AtomicU32, Ordering};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::handle::{guard_int, guard_ptr};
/// `"handshake"`.
pub const TYPE_HANDSHAKE: &str = "handshake";
/// `"load_graph"`.
pub const TYPE_LOAD_GRAPH: &str = "load_graph";
/// `"render_frame"`.
pub const TYPE_RENDER_FRAME: &str = "render_frame";
/// `"frame_ready"`.
pub const TYPE_FRAME_READY: &str = "frame_ready";
/// `"cancel"`.
pub const TYPE_CANCEL: &str = "cancel";
/// `"graph_update"`.
pub const TYPE_GRAPH_UPDATE: &str = "graph_update";
/// `"shutdown"`.
pub const TYPE_SHUTDOWN: &str = "shutdown";
/// `"error"`.
pub const TYPE_ERROR: &str = "error";
/// `handshake` — field-for-field equivalent of `oak_ipc_handshake`
/// (ipc.h). Wire field names match the C++ serializer.
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct HandshakeMsg {
/// Protocol version.
pub protocol_version: i32,
/// Worker->main output shared-memory segment key.
pub shm_key: String,
/// Main->worker input shared-memory segment key (optional).
pub input_shm_key: String,
/// Number of main->worker input frame slots.
pub input_slots: i32,
/// Number of worker->main output frame slots.
pub output_slots: i32,
/// Per-output-slot pixel block size.
pub slot_data_bytes: i64,
/// Per-input-slot pixel block size.
pub input_slot_data_bytes: i64,
}
impl HandshakeMsg {
/// The worker's startup handshake (`worker.cpp startup_handshake()`).
pub fn to_json(&self) -> Value {
json!({
"type": TYPE_HANDSHAKE,
"protocol_version": self.protocol_version,
"shm_key": self.shm_key,
"input_shm_key": self.input_shm_key,
"input_slots": self.input_slots,
"output_slots": self.output_slots,
"slot_data_bytes": self.slot_data_bytes,
"input_slot_data_bytes": self.input_slot_data_bytes,
})
}
}
/// `render_frame` — request a frame render. Wire names per ipcmessage.cpp:
/// `ticket`, `node`, `channels` (not the ipc.h POD names).
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct RenderFrameMsg {
/// Correlates with the eventual frame_ready.
pub ticket: i64,
/// Viewer node stable uuid in the loaded graph.
pub node: String,
/// Frame timestamp numerator.
pub time_num: i64,
/// Frame timestamp denominator.
pub time_den: i64,
/// Forced output size (0 = graph default).
pub width: i32,
/// Forced output height (0 = graph default).
pub height: i32,
/// Forced PixelFormat (-1 = default).
pub format: i32,
/// Channel count (0 = default).
pub channels: i32,
/// RenderMode.
pub mode: i32,
/// Optional decoded input slot (-1 = none).
pub input_slot: i32,
/// Ordered decoded input slots.
pub input_slots: Vec<i32>,
/// Output color transform present?
pub has_color_transform: bool,
/// Color transform targets the display space.
pub color_is_display: bool,
/// Output color space name.
pub color_output: String,
/// Output color view name.
pub color_view: String,
/// Output color look name.
pub color_look: String,
}
/// `frame_ready` — a rendered frame is published (wire names `ticket`/
/// `slot`).
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct FrameReadyMsg {
/// Correlates with the render_frame request.
pub ticket: i64,
/// Index into the worker->main output FrameSlotPool.
pub slot: i32,
}
/// `cancel` — abandon an in-flight ticket by id.
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct CancelMsg {
/// The in-flight ticket id to abandon.
pub ticket: i64,
}
/// `load_graph` — path to a temporary file holding the serialized graph.
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
#[serde(default)]
pub struct LoadGraphMsg {
/// Path to the temporary file holding the serialized graph.
pub path: String,
}
/// Build a worker-side error report, mirroring `error_message()` in
/// worker.cpp: `{"type":"error","message":...}` plus `"ticket"` when
/// non-zero.
pub fn error_message(message: &str, ticket: Option<i64>) -> Value {
match ticket.filter(|t| *t != 0) {
Some(t) => json!({ "type": TYPE_ERROR, "message": message, "ticket": t }),
None => json!({ "type": TYPE_ERROR, "message": message }),
}
}
/// Write one NDJSON message line (compact JSON + `\n`), the Rust port of
/// `ipcmessage.cpp write_message()`.
pub fn write_message(w: &mut impl Write, msg: &Value) -> io::Result<()> {
let line =
serde_json::to_string(msg).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
w.write_all(line.as_bytes())?;
w.write_all(b"\n")
}
// ---------------------------------------------------------------------------
// Shared-memory frame-slot transport
// ---------------------------------------------------------------------------
/// `OAK_IPC_SHM_KEY_CAP` — capacity of shm key strings (ipc.h), incl. NUL.
pub const OAK_IPC_SHM_KEY_CAP: usize = 128;
/// `OAK_IPC_COLORSPACE_CAP` — capacity of `oak_frame_slot_meta::colorspace`.
pub const OAK_IPC_COLORSPACE_CAP: usize = 128;
/// Byte alignment of every sub-region of a frame slot pool (the C++
/// `k_align = 64`; cache-line alignment).
const K_ALIGN: usize = 64;
/// `k_magic = 0x4F4B5350` ("OKSP") — the frame slot pool header magic.
pub const FRAMEPOOL_MAGIC: u32 = 0x4F4B5350;
/// Round `value` up to the next multiple of `align` (power of two).
const fn align_up(value: usize, align: usize) -> usize {
(value + (align - 1)) & !(align - 1)
}
/// `OAK_IPC_SHM_MODE_CREATE` / `OAK_IPC_SHM_MODE_ATTACH` (ipc.h).
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShmMode {
/// Create (and own) the segment. Fails if it already exists; the owner
/// unlinks it on close.
Create,
/// Attach to a segment created by the peer. Does not unlink on close.
Attach,
}
impl ShmMode {
/// Map the C ABI mode integer (`OAK_IPC_SHM_MODE_CREATE` = 0,
/// `OAK_IPC_SHM_MODE_ATTACH` = 1) back to the enum.
fn from_c(v: c_int) -> ShmMode {
match v {
0 => ShmMode::Create,
_ => ShmMode::Attach,
}
}
}
/// Per-slot metadata describing the frame currently occupying a slot —
/// field-for-field `oak_frame_slot_meta` from `engine/include/oakengine/ipc.h`.
///
/// This POD lives in shared memory alongside the pixel data and is part of
/// the version-1 wire protocol; `#[repr(C)]` keeps the C ABI layout.
#[repr(C)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct FrameSlotMeta {
/// Caller-defined tag (ticket id, or footage stream hash).
pub id: i64,
/// Frame timestamp numerator.
pub time_num: i64,
/// Frame timestamp denominator.
pub time_den: i64,
/// Frame width.
pub width: i32,
/// Frame height.
pub height: i32,
/// `PixelFormat::Format` value.
pub format: i32,
/// Channel count.
pub channel_count: i32,
/// Bytes per scanline (stride).
pub linesize: i32,
/// Valid bytes written into the slot's data block.
pub data_size: i32,
/// Input colorspace name.
pub colorspace: [c_char; OAK_IPC_COLORSPACE_CAP],
}
impl Default for FrameSlotMeta {
fn default() -> Self {
FrameSlotMeta {
id: 0,
time_num: 0,
time_den: 0,
width: 0,
height: 0,
format: 0,
channel_count: 0,
linesize: 0,
data_size: 0,
colorspace: [0; OAK_IPC_COLORSPACE_CAP],
}
}
}
/// `sizeof(oak_frame_slot_meta)` (8+8+8 + 4*6 + 128).
const FRAME_SLOT_META_SIZE: usize = 176;
// ---------------------------------------------------------------------------
// SpscRingBuffer
// ---------------------------------------------------------------------------
/// A lock-free single-producer / single-consumer ring buffer of `u32`
/// indices, living in shared memory — the port of
/// `engine/include/oakengine/spscringbuffer.h`.
///
/// Layout (offsets from the buffer base, matching the C++ class):
///
/// ```text
/// 0 head_ u32 producer cursor (relaxed read, release write)
/// 4 tail_ u32 consumer cursor (relaxed read, release write)
/// 8 capacity_ u32 slot count (written once by create())
/// 12 slots u32[capacity]
/// ```
///
/// One slot is always left empty to disambiguate full and empty, so a
/// buffer with `capacity` slots holds at most `capacity - 1` live entries.
/// The payload is a `u32` slot index — never a pointer.
///
/// `SpscRingBuffer` is a thin view over a raw pointer; it is `Copy` and
/// owns nothing. All methods are `unsafe` because they read and write the
/// shared segment concurrently with a peer process.
#[derive(Clone, Copy)]
pub struct SpscRingBuffer {
/// Base of the ring header (`head_` at offset 0).
base: *mut u8,
}
// The shared memory the ring lives in is usable from any thread of the
// local process; synchronization with the peer is the ring's own atomics.
unsafe impl Send for SpscRingBuffer {}
unsafe impl Sync for SpscRingBuffer {}
impl SpscRingBuffer {
/// `sizeof(SpscRingBuffer)` — header bytes before the slot array.
pub const HEADER_BYTES: usize = 12;
/// Total bytes required for the header plus `capacity` index slots
/// (`SpscRingBuffer::bytes_needed`).
pub fn bytes_needed(capacity: u32) -> usize {
Self::HEADER_BYTES + capacity as usize * 4
}
/// In-place construct a ring header at `mem` with `capacity` index
/// slots. `mem` must provide at least [`Self::bytes_needed`] bytes and
/// be suitably aligned (mmap-backed segments are). Done exactly once by
/// whichever process owns the segment's creation; the peer uses
/// [`Self::attach`] instead.
///
/// # Safety
/// `mem` must be a valid, writable, aligned buffer of at least
/// [`Self::bytes_needed`] bytes, and must not be concurrently written
/// during this call.
pub unsafe fn create(mem: *mut u8, capacity: u32) -> SpscRingBuffer {
let ring = SpscRingBuffer { base: mem };
unsafe {
ring.store_capacity(capacity);
ring.head().store(0, Ordering::Relaxed);
ring.tail().store(0, Ordering::Relaxed);
for i in 0..capacity as usize {
*ring.slot_ptr(i) = 0;
}
}
ring
}
/// Re-interpret already-initialized shared memory as a ring buffer
/// (peer-process side). No writes are performed.
///
/// # Safety
/// `mem` must point to a buffer previously initialized by
/// [`Self::create`] (or an ABI-identical C++ side) that stays mapped
/// for as long as this view is used.
pub unsafe fn attach(mem: *mut u8) -> SpscRingBuffer {
SpscRingBuffer { base: mem }
}
/// The ring's capacity (slot count).
///
/// # Safety
/// `self` must point at a live ring (created or attached).
pub unsafe fn capacity(&self) -> u32 {
unsafe { (self.base.add(8) as *const u32).read() }
}
/// Producer side: enqueue an index. Returns false if the buffer is full.
///
/// # Safety
/// Exactly one producer may call this concurrently with exactly one
/// consumer calling [`Self::pop`]; the ring must be live.
pub unsafe fn push(&self, value: u32) -> bool {
unsafe {
let head = self.head().load(Ordering::Relaxed);
let next = self.increment(head);
if next == self.tail().load(Ordering::Acquire) {
return false;
}
*self.slot_ptr(head as usize) = value;
self.head().store(next, Ordering::Release);
}
true
}
/// Consumer side: dequeue an index into `out`. Returns false if the
/// buffer is empty.
///
/// # Safety
/// Exactly one consumer may call this concurrently with exactly one
/// producer calling [`Self::push`]; the ring must be live.
pub unsafe fn pop(&self, out: &mut u32) -> bool {
unsafe {
let tail = self.tail().load(Ordering::Relaxed);
if tail == self.head().load(Ordering::Acquire) {
return false;
}
*out = *self.slot_ptr(tail as usize);
self.tail().store(self.increment(tail), Ordering::Release);
}
true
}
/// Approximate number of entries currently queued; may be stale the
/// instant it returns. For metrics/backpressure, not correctness.
///
/// # Safety
/// The ring must be live.
pub unsafe fn size_approx(&self) -> u32 {
unsafe {
let head = self.head().load(Ordering::Acquire);
let tail = self.tail().load(Ordering::Acquire);
let cap = self.capacity();
(head + cap - tail) % cap
}
}
/// Approximate empty check (see [`Self::size_approx`]).
///
/// # Safety
/// The ring must be live.
pub unsafe fn is_empty_approx(&self) -> bool {
unsafe { self.head().load(Ordering::Acquire) == self.tail().load(Ordering::Acquire) }
}
#[inline]
fn increment(&self, index: u32) -> u32 {
// `capacity_` is small; this avoids requiring a power-of-two capacity.
unsafe { (index + 1) % self.capacity() }
}
#[inline]
unsafe fn head(&self) -> &AtomicU32 {
unsafe { &*(self.base as *const AtomicU32) }
}
#[inline]
unsafe fn tail(&self) -> &AtomicU32 {
unsafe { &*(self.base.add(4) as *const AtomicU32) }
}
#[inline]
unsafe fn store_capacity(&self, capacity: u32) {
unsafe { *(self.base.add(8) as *mut u32) = capacity };
}
#[inline]
unsafe fn slot_ptr(&self, index: usize) -> *mut u32 {
unsafe { self.base.add(Self::HEADER_BYTES + index * 4) as *mut u32 }
}
}
// ---------------------------------------------------------------------------
// FrameSlotPool
// ---------------------------------------------------------------------------
/// Pool header written by create() and read back by attach(). Field-for-
/// field the C++ `FrameSlotPool::Header` (offsets: 0,4,8,16,24,32,40;
/// 48 bytes total).
#[repr(C)]
struct PoolHeader {
magic: u32,
slot_count: u32,
slot_data_bytes: u64,
free_ring_offset: u64,
ready_ring_offset: u64,
meta_offset: u64,
data_offset: u64,
}
const POOL_HEADER_SIZE: usize = 48;
/// A fixed-size pool of equal-sized frame slots in shared memory with
/// lock-free hand-off — the port of the C++ `FrameSlotPool`
/// (`engine/src/oliveimpl/render/ipc/frameslotpool.{h,cpp}`).
///
/// One pool models a single direction of frame flow. It does NOT own the
/// memory; it is a view over a mapped [`SharedMemoryRegion`] (or any
/// ABI-identical segment). Lifecycle: the filler `acquire`s a free slot,
/// writes meta + pixels, then `publish`es it; the drainer `consume`s the
/// next ready slot, reads it, and `release`s it back to the free ring.
///
/// [`FrameSlotPool`] is `Clone` — the clone is another view of the same
/// segment (the C++ `copy()`), useful to hand both sides a handle without
/// owning the mapping twice.
pub struct FrameSlotPool {
/// Segment base.
base: *mut u8,
/// The pool header at `base + 0`.
header: *mut PoolHeader,
/// Free-ring view (filler pops, drainer pushes).
free_ring: SpscRingBuffer,
/// Ready-ring view (filler pushes, drainer pops).
ready_ring: SpscRingBuffer,
/// Metadata array at `base + meta_offset`.
meta: *mut FrameSlotMeta,
/// Pixel data blocks at `base + data_offset`.
data: *mut u8,
}
// Views into shared memory are safe to share within the process; the rings
// carry their own synchronization.
unsafe impl Send for FrameSlotPool {}
unsafe impl Sync for FrameSlotPool {}
impl Clone for FrameSlotPool {
fn clone(&self) -> FrameSlotPool {
FrameSlotPool {
base: self.base,
header: self.header,
free_ring: self.free_ring,
ready_ring: self.ready_ring,
meta: self.meta,
data: self.data,
}
}
}
impl FrameSlotPool {
/// Total bytes a region must provide to back a pool of
/// `slot_count` x `slot_data_bytes`
/// (`FrameSlotPool::bytes_needed`).
pub fn bytes_needed(slot_count: u32, slot_data_bytes: usize) -> usize {
let ring_cap = slot_count + 1;
let mut total = align_up(POOL_HEADER_SIZE, K_ALIGN);
let ring_bytes = align_up(SpscRingBuffer::bytes_needed(ring_cap), K_ALIGN);
total += ring_bytes; // free ring
total += ring_bytes; // ready ring
total += align_up(FRAME_SLOT_META_SIZE * slot_count as usize, K_ALIGN); // metadata
total += align_up(slot_data_bytes, K_ALIGN) * slot_count as usize; // pixel data
total
}
/// Lay out and initialize a brand-new pool over `mem` (owner side, once).
///
/// Writes the header, initializes both rings, seeds the free ring with
/// every slot index and zeroes the metadata. `mem` must provide at
/// least [`Self::bytes_needed`] bytes of writable, aligned memory (an
/// mmap-backed segment) and must outlive the returned pool.
///
/// # Safety
/// `mem` must be a valid, writable, aligned buffer of at least
/// [`Self::bytes_needed`] bytes, not concurrently written during this
/// call.
pub unsafe fn create(mem: *mut u8, slot_count: u32, slot_data_bytes: usize) -> FrameSlotPool {
let ring_cap = slot_count + 1;
let free_off = align_up(POOL_HEADER_SIZE, K_ALIGN);
let ready_off = free_off + align_up(SpscRingBuffer::bytes_needed(ring_cap), K_ALIGN);
let meta_off = ready_off + align_up(SpscRingBuffer::bytes_needed(ring_cap), K_ALIGN);
let data_off = meta_off + align_up(FRAME_SLOT_META_SIZE * slot_count as usize, K_ALIGN);
let pool = unsafe {
FrameSlotPool {
base: mem,
header: mem as *mut PoolHeader,
free_ring: SpscRingBuffer::create(mem.add(free_off), ring_cap),
ready_ring: SpscRingBuffer::create(mem.add(ready_off), ring_cap),
meta: mem.add(meta_off) as *mut FrameSlotMeta,
data: mem.add(data_off),
}
};
unsafe {
(*pool.header).magic = FRAMEPOOL_MAGIC;
(*pool.header).slot_count = slot_count;
(*pool.header).slot_data_bytes = slot_data_bytes as u64;
(*pool.header).free_ring_offset = free_off as u64;
(*pool.header).ready_ring_offset = ready_off as u64;
(*pool.header).meta_offset = meta_off as u64;
(*pool.header).data_offset = data_off as u64;
}
// `ptr::write_bytes` counts in elements of T, so cast to bytes.
unsafe {
ptr::write_bytes(
pool.meta as *mut u8,
0,
slot_count as usize * std::mem::size_of::<FrameSlotMeta>(),
);
}
// Seed the free ring with every slot index so the filler can
// acquire() immediately.
for i in 0..slot_count {
unsafe { pool.free_ring.push(i) };
}
pool
}
/// Map an existing, already-initialized pool (peer side).
///
/// Reads the geometry from the in-memory header written by
/// [`Self::create`]; the returned pool reports `is_valid() == false`
/// when the magic does not match.
///
/// # Safety
/// `mem` must point to a mapped segment that either contains a pool
/// initialized by [`Self::create`] (or an ABI-identical C++ side) or is
/// an arbitrary buffer whose first 4 bytes we must be able to read.
pub unsafe fn attach(mem: *mut u8) -> FrameSlotPool {
if mem.is_null() {
return FrameSlotPool::invalid();
}
let header = mem as *mut PoolHeader;
// SAFETY: `mem` is a live mapping of at least the header size.
if unsafe { (*header).magic } != FRAMEPOOL_MAGIC {
return FrameSlotPool::invalid();
}
let pool = unsafe {
FrameSlotPool {
base: mem,
header,
free_ring: SpscRingBuffer::attach(mem.add((*header).free_ring_offset as usize)),
ready_ring: SpscRingBuffer::attach(mem.add((*header).ready_ring_offset as usize)),
meta: mem.add((*header).meta_offset as usize) as *mut FrameSlotMeta,
data: mem.add((*header).data_offset as usize),
}
};
pool
}
/// An invalid pool (attach on a non-pool segment).
fn invalid() -> FrameSlotPool {
FrameSlotPool {
base: ptr::null_mut(),
header: ptr::null_mut(),
free_ring: SpscRingBuffer {
base: ptr::null_mut(),
},
ready_ring: SpscRingBuffer {
base: ptr::null_mut(),
},
meta: ptr::null_mut(),
data: ptr::null_mut(),
}
}
/// True when the pool was attached to a segment containing a valid pool
/// header.
pub fn is_valid(&self) -> bool {
!self.header.is_null()
}
/// Number of slots in the pool (0 for an invalid pool).
pub fn slot_count(&self) -> u32 {
if self.is_valid() {
unsafe { (*self.header).slot_count }
} else {
0
}
}
/// Bytes available in every slot's pixel-data block (0 for invalid).
pub fn slot_data_bytes(&self) -> usize {
if self.is_valid() {
unsafe { (*self.header).slot_data_bytes as usize }
} else {
0
}
}
/// Byte stride between consecutive slot data blocks.
fn slot_stride(&self) -> usize {
align_up(self.slot_data_bytes(), K_ALIGN)
}
// ---- Filler side ----
/// Take ownership of a free slot. Returns false (leaving `index`
/// untouched) if none is free.
///
/// # Safety
/// The pool must be a valid view of a live segment.
pub unsafe fn acquire(&self, index: &mut u32) -> bool {
unsafe { self.free_ring.pop(index) }
}
/// Pointer to a slot's pixel data block (`slot_data_bytes` available).
///
/// # Safety
/// `index` must be in `0..slot_count`; the pool must be a valid view of
/// a live segment.
pub unsafe fn slot_data(&self, index: u32) -> *mut u8 {
unsafe { self.data.add(index as usize * self.slot_stride()) }
}
/// Mutable metadata for a slot. The filler writes this before
/// [`Self::publish`]. The returned pointer addresses shared memory; it
/// is borrowed, not owned.
///
/// # Safety
/// `index` must be in `0..slot_count`; the pool must be a valid view of
/// a live segment.
pub unsafe fn meta(&self, index: u32) -> *mut FrameSlotMeta {
unsafe { self.meta.add(index as usize) }
}
/// Publish a filled slot to the drainer. Must follow a successful
/// [`Self::acquire`] of `index`. Returns false if the ready ring is
/// full (the filler must then release the slot and retry later).
///
/// # Safety
/// `index` must be a slot previously acquired and not yet released.
pub unsafe fn publish(&self, index: u32) -> bool {
unsafe { self.ready_ring.push(index) }
}
// ---- Drainer side ----
/// Take the next published slot. Returns false if nothing is ready.
///
/// # Safety
/// The pool must be a valid view of a live segment.
pub unsafe fn consume(&self, index: &mut u32) -> bool {
unsafe { self.ready_ring.pop(index) }
}
/// Return a consumed slot to the free pool for reuse. Must follow a
/// successful [`Self::consume`] of `index`. Returns false if the free
/// ring is full (the drainer must not release the slot yet).
///
/// # Safety
/// `index` must be a slot previously consumed and not yet re-acquired.
pub unsafe fn release(&self, index: u32) -> bool {
unsafe { self.free_ring.push(index) }
}
/// Immutable metadata for a slot (drainer side).
///
/// # Safety
/// `index` must be in `0..slot_count`; the pool must be a valid view of
/// a live segment.
pub unsafe fn meta_const(&self, index: u32) -> *const FrameSlotMeta {
unsafe { self.meta.add(index as usize) }
}
/// Immutable pixel data for a slot.
///
/// # Safety
/// `index` must be in `0..slot_count`; the pool must be a valid view of
/// a live segment.
pub unsafe fn slot_data_const(&self, index: u32) -> *const u8 {
unsafe { self.data.add(index as usize * self.slot_stride()) }
}
}
// ---------------------------------------------------------------------------
// SharedMemoryRegion
// ---------------------------------------------------------------------------
/// A named, fixed-size POSIX shared-memory segment mapped into the process
/// address space — the port of the C++ `SharedMemoryRegion`
/// (`engine/render/ipc/sharedmemoryregion.cpp`).
///
/// One process opens the segment in [`ShmMode::Create`] (owner: fails if
/// the name already exists, zeroes the mapping, unlinks on close); the
/// peer opens the same key in [`ShmMode::Attach`]. The mapping is a raw
/// contiguous byte range; the ring buffers and frame slot pools are laid
/// out inside it. Nothing here is locked — synchronization is entirely the
/// caller's responsibility via the lock-free structures placed in the
/// mapping.
pub struct SharedMemoryRegion {
/// The key the region was opened with (no leading slash).
key: String,
/// Requested mapping size in bytes.
size: usize,
/// The mmap'd data pointer; null when invalid.
data: *mut u8,
/// File descriptor from `shm_open` (-1 when invalid).
fd: i32,
/// Open mode.
mode: ShmMode,
/// Human-readable reason of the last failed open.
error: String,
/// The platform-prefixed name actually passed to `shm_open`.
shm_name: String,
}
impl SharedMemoryRegion {
/// An empty (invalid) region.
pub fn new() -> SharedMemoryRegion {
SharedMemoryRegion {
key: String::new(),
size: 0,
data: ptr::null_mut(),
fd: -1,
mode: ShmMode::Attach,
error: String::new(),
shm_name: String::new(),
}
}
/// Build a unique segment key for a worker, e.g.
/// "olive-rw-<pid>-<index>" (`SharedMemoryRegion::make_key`).
/// Centralized so the owner and the spawned worker agree on the same
/// name.
pub fn make_key(owner_pid: i64, worker_index: i32) -> String {
format!("olive-rw-{owner_pid}-{worker_index}")
}
/// Open the segment identified by `key` with the given `size` in bytes.
///
/// `key` is a short identifier (no leading slash needed; the platform
/// prefix is added internally). Returns true on success; on failure
/// [`Self::error`] carries a human-readable reason. An existing region
/// is closed first.
pub fn open(&mut self, key: &str, size: usize, mode: ShmMode) -> bool {
self.close();
self.key = key.to_string();
self.size = size;
self.mode = mode;
// POSIX shared-memory names must start with a single slash and
// contain no others.
let shm_name = format!("/{}", key.replace('/', "_"));
let name_c = match std::ffi::CString::new(shm_name.clone()) {
Ok(c) => c,
Err(_) => {
self.error = format!("invalid shm key {key:?} (contains NUL)");
return false;
}
};
self.shm_name = shm_name;
let mut oflag = libc::O_RDWR;
if mode == ShmMode::Create {
oflag |= libc::O_CREAT | libc::O_EXCL;
// Clear any stale segment left by a crashed previous run with
// the same name.
unsafe { libc::shm_unlink(name_c.as_ptr()) };
}
let fd = unsafe { libc::shm_open(name_c.as_ptr(), oflag, 0o600) };
if fd < 0 {
self.error = format!(
"shm_open({}) failed: {}",
self.shm_name,
std::io::Error::last_os_error()
);
return false;
}
self.fd = fd;
if mode == ShmMode::Create {
if unsafe { libc::ftruncate(fd, size as libc::off_t) } != 0 {
self.error = format!("ftruncate failed: {}", std::io::Error::last_os_error());
self.close();
return false;
}
} else {
// mmap() succeeds even beyond the real segment size and only
// faults (SIGBUS) on access, so verify the segment is large
// enough up front.
let mut st: libc::stat = unsafe { std::mem::zeroed() };
if unsafe { libc::fstat(fd, &mut st) } != 0 {
self.error = format!("fstat failed: {}", std::io::Error::last_os_error());
self.close();
return false;
}
if (st.st_size as usize) < size {
self.error = format!(
"shared memory segment is {} bytes, smaller than the requested {}",
st.st_size, size
);
self.close();
return false;
}
}
let data = unsafe {
libc::mmap(
ptr::null_mut(),
size,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_SHARED,
fd,
0,
)
};
if data == libc::MAP_FAILED {
self.error = format!("mmap failed: {}", std::io::Error::last_os_error());
self.close();
return false;
}
self.data = data as *mut u8;
self.error.clear();
if mode == ShmMode::Create {
unsafe { ptr::write_bytes(self.data, 0, size) };
}
true
}
/// Unmap and (if owner) unlink the segment. Also called by `Drop`.
pub fn close(&mut self) {
if !self.data.is_null() {
unsafe { libc::munmap(self.data as *mut std::ffi::c_void, self.size) };
self.data = ptr::null_mut();
}
if self.fd >= 0 {
unsafe { libc::close(self.fd) };
self.fd = -1;
}
if self.mode == ShmMode::Create && !self.shm_name.is_empty() {
// Only the owner unlinks, so the name is freed once both sides
// have unmapped.
if let Ok(c) = std::ffi::CString::new(self.shm_name.clone()) {
unsafe { libc::shm_unlink(c.as_ptr()) };
}
self.shm_name.clear();
}
self.size = 0;
}
/// True when the region holds a live mapping.
pub fn is_valid(&self) -> bool {
!self.data.is_null()
}
/// The mapped data pointer (null when invalid).
pub fn data(&self) -> *mut u8 {
self.data
}
/// The mapping size in bytes.
pub fn size(&self) -> usize {
self.size
}
/// The key the region was opened with.
pub fn key(&self) -> &str {
&self.key
}
/// Human-readable reason of the last failed open.
pub fn error(&self) -> &str {
&self.error
}
}
impl Default for SharedMemoryRegion {
fn default() -> Self {
SharedMemoryRegion::new()
}
}
impl Drop for SharedMemoryRegion {
fn drop(&mut self) {
self.close();
}
}
#[cfg(test)]
mod tests {
use super::*;
// ---- Control-plane protocol ------------------------------------------
#[test]
fn handshake_wire_format_matches_cpp_field_names() {
let hs = HandshakeMsg {
protocol_version: 1,
shm_key: "olive-rw-1234-0-out".into(),
input_shm_key: "".into(),
input_slots: 0,
output_slots: 6,
slot_data_bytes: 4096,
input_slot_data_bytes: 0,
};
let value = hs.to_json();
// Key order is not part of the contract (JSON objects; the C++
// QJsonObject is hash-ordered too), but the names must match the
// C++ serializer exactly.
assert_eq!(value["type"], "handshake");
assert_eq!(value["protocol_version"], 1);
assert_eq!(value["shm_key"], "olive-rw-1234-0-out");
assert_eq!(value["input_shm_key"], "");
assert_eq!(value["input_slots"], 0);
assert_eq!(value["output_slots"], 6);
assert_eq!(value["slot_data_bytes"], 4096);
assert_eq!(value["input_slot_data_bytes"], 0);
// And the serialized line must parse back to the same object.
let round: serde_json::Value =
serde_json::from_str(&serde_json::to_string(&value).unwrap()).unwrap();
assert_eq!(round, value);
}
#[test]
fn render_frame_parse_accepts_cpp_field_names() {
let json = r#"{"type":"render_frame","ticket":42,"node":"abcd","time_num":1,"time_den":24,"width":1920,"height":1080,"format":-1,"channels":0,"mode":0,"input_slot":-1,"input_slots":[],"has_color_transform":false,"color_output":"","color_view":"","color_look":""}"#;
let m: RenderFrameMsg = serde_json::from_str(json).unwrap();
assert_eq!(m.ticket, 42);
assert_eq!(m.node, "abcd");
assert_eq!(m.time_num, 1);
assert_eq!(m.time_den, 24);
assert_eq!(m.width, 1920);
assert_eq!(m.input_slot, -1);
}
#[test]
fn render_frame_defaults_on_missing_fields() {
// The C++ parser defaults missing fields (QJsonValue defaults);
// serde(default) mirrors that.
let m: RenderFrameMsg =
serde_json::from_str(r#"{"type":"render_frame","ticket":7}"#).unwrap();
assert_eq!(m.ticket, 7);
assert_eq!(m.time_den, 0);
assert!(m.node.is_empty());
assert!(!m.has_color_transform);
}
#[test]
fn error_message_carries_ticket_only_when_nonzero() {
assert_eq!(
error_message("boom", None),
json!({ "type": "error", "message": "boom" })
);
assert_eq!(
error_message("boom", Some(0)),
json!({ "type": "error", "message": "boom" })
);
assert_eq!(
error_message("boom", Some(9)),
json!({ "type": "error", "message": "boom", "ticket": 9 })
);
}
#[test]
fn write_message_emits_one_json_line() {
let mut buf = Vec::new();
write_message(&mut buf, &json!({ "type": "shutdown" })).unwrap();
assert_eq!(String::from_utf8(buf).unwrap(), "{\"type\":\"shutdown\"}\n");
}
// ---- Shared-memory transport -----------------------------------------
/// A unique, temporary POSIX segment key for a test (pid + counter), so
/// parallel test runs never collide.
fn test_key(name: &str) -> String {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
SharedMemoryRegion::make_key(i64::from(std::process::id()), (n & 0x7FFF) as i32)
+ &format!("-{name}")
}
/// Create one segment and map it a second time — the in-process
/// equivalent of two processes sharing a segment. Returns
/// `(owner_region, peer_region)`; both must be kept alive for the
/// whole test (the peer is an attach that does not unlink).
fn two_mappings(key: &str, size: usize) -> (SharedMemoryRegion, SharedMemoryRegion) {
let mut owner = SharedMemoryRegion::new();
assert!(
owner.open(key, size, ShmMode::Create),
"create failed: {}",
owner.error()
);
let mut peer = SharedMemoryRegion::new();
assert!(
peer.open(key, size, ShmMode::Attach),
"attach failed: {}",
peer.error()
);
(owner, peer)
}
// ---- SpscRingBuffer -------------------------------------------------
#[test]
fn ring_bytes_needed_matches_cpp_layout() {
// 12 header bytes + capacity * 4.
assert_eq!(SpscRingBuffer::bytes_needed(4), 12 + 16);
assert_eq!(SpscRingBuffer::bytes_needed(5), 12 + 20);
assert_eq!(SpscRingBuffer::bytes_needed(0), 12);
}
#[test]
fn ring_empty_full_and_single_entry() {
let key = test_key("ring-empty");
let size = SpscRingBuffer::bytes_needed(4);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: both mappings are live and at least `size` bytes.
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
assert!(unsafe { cons.is_empty_approx() });
let mut v = 99;
assert!(!unsafe { cons.pop(&mut v) });
assert_eq!(v, 99);
assert!(unsafe { prod.push(7) });
assert!(!unsafe { cons.is_empty_approx() });
assert_eq!(unsafe { cons.size_approx() }, 1);
assert!(unsafe { cons.pop(&mut v) });
assert_eq!(v, 7);
assert!(unsafe { cons.is_empty_approx() });
}
#[test]
fn ring_capacity_minus_one_live_entries() {
// A ring of capacity N holds at most N-1 entries (one slot is
// always left empty to tell full from empty).
let key = test_key("ring-cap");
let size = SpscRingBuffer::bytes_needed(4);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
for i in 0..3 {
assert!(unsafe { prod.push(i) });
}
// The 4th push must fail: head would collide with tail.
assert!(!unsafe { prod.push(99) });
let mut v = 0;
for expected in 0..3 {
assert!(unsafe { cons.pop(&mut v) });
assert_eq!(v, expected);
}
assert!(!unsafe { cons.pop(&mut v) });
}
#[test]
fn ring_wraparound_preserves_order() {
// Fill, drain, then wrap past the end of the slot array: cursors
// are modulo-capacity, order must be preserved across the wrap.
let key = test_key("ring-wrap");
let size = SpscRingBuffer::bytes_needed(4);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
for i in 0..3 {
assert!(unsafe { prod.push(i) });
}
let mut v = 0;
for _ in 0..3 {
assert!(unsafe { cons.pop(&mut v) });
}
// Ring is empty again; push past the wrap point.
for i in 3..6 {
assert!(unsafe { prod.push(i) });
}
for expected in 3..6 {
assert!(unsafe { cons.pop(&mut v) });
assert_eq!(v, expected);
}
}
// ---- FrameSlotPool --------------------------------------------------
#[test]
fn framepool_bytes_needed_matches_cpp_offsets() {
// Recompute by hand with the C++ layout: header 64, each ring
// align_up(12 + 4*(n+1), 64), meta align_up(176*n, 64), data
// align_up(slot_bytes, 64) * n.
let check = |n: u32, slot: usize| {
let ring = align_up(12 + 4 * (n as usize + 1), 64);
let expected =
64 + ring + ring + align_up(176 * n as usize, 64) + align_up(slot, 64) * n as usize;
assert_eq!(FrameSlotPool::bytes_needed(n, slot), expected);
};
check(4, 4096);
check(6, 1_000_000);
check(1, 64);
check(3, 100);
}
#[test]
fn framepool_create_attach_two_processes_both_directions() {
// "Two processes": two mappings of the same segment. Owner creates
// the pool; the peer attaches. A filler on one side and a drainer
// on the other exchange slots in both directions.
let key = test_key("pool-bidi");
let slots = 4u32;
let slot_bytes = 64usize;
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: both mappings are live and sized by bytes_needed.
let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
let drainer = unsafe { FrameSlotPool::attach(peer.data()) };
assert!(filler.is_valid());
assert!(drainer.is_valid());
assert_eq!(drainer.slot_count(), slots);
assert_eq!(drainer.slot_data_bytes(), slot_bytes);
// Filler acquires every slot exactly once (seeded free ring), then
// the free ring is empty.
let mut got = Vec::new();
for _ in 0..slots {
let mut s = 0;
assert!(unsafe { filler.acquire(&mut s) });
got.push(s);
}
got.sort_unstable();
assert_eq!(got, vec![0, 1, 2, 3]);
let mut extra = 0;
assert!(!unsafe { filler.acquire(&mut extra) });
// Drainer sees nothing ready yet.
assert!(!unsafe { drainer.consume(&mut extra) });
// Filler writes pixels + meta into two slots and publishes them.
for (i, slot) in [0u32, 2u32].iter().enumerate() {
// SAFETY: `slot` was acquired above.
let data = unsafe { filler.slot_data(*slot) };
unsafe { ptr::write_bytes(data, (i * 40 + 1) as u8, slot_bytes) };
// SAFETY: slot in range.
let meta = unsafe { &mut *filler.meta(*slot) };
meta.id = 100 + *slot as i64;
meta.width = 8;
meta.height = 8;
meta.data_size = slot_bytes as i32;
assert!(unsafe { filler.publish(*slot) });
}
// Drainer consumes them through its own mapping and sees the same
// payloads and metadata.
let mut consumed = Vec::new();
for _ in 0..2 {
let mut s = 0;
assert!(unsafe { drainer.consume(&mut s) });
// SAFETY: s was consumed.
let data = unsafe { drainer.slot_data_const(s) };
let meta = unsafe { &*drainer.meta_const(s) };
assert_eq!(meta.id, 100 + s as i64);
assert_eq!(meta.width, 8);
assert_eq!(meta.data_size, slot_bytes as i32);
// SAFETY: slot_bytes readable in the slot block.
let first = unsafe { *data };
assert_eq!(first, ((s as usize / 2) * 40 + 1) as u8);
consumed.push(s);
}
consumed.sort_unstable();
assert_eq!(consumed, vec![0, 2]);
assert!(!unsafe { drainer.consume(&mut extra) });
// Drainer releases the slots back; the filler can acquire them
// again — the full round trip through both rings.
for s in consumed {
assert!(unsafe { drainer.release(s) });
}
let mut s = 0;
assert!(unsafe { filler.acquire(&mut s) });
assert_eq!(s, 0);
}
#[test]
fn framepool_wraparound_and_full_edges() {
// Small pool: cycle every slot many times, verifying the rings'
// modulo behavior end to end.
let key = test_key("pool-wrap");
let slots = 3u32;
let slot_bytes = 32usize;
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
let drainer = unsafe { FrameSlotPool::attach(peer.data()) };
for cycle in 0..4u32 {
let mut published = Vec::new();
for _ in 0..slots {
let mut s = 0;
assert!(unsafe { filler.acquire(&mut s) }, "cycle {cycle}");
// SAFETY: acquired slot.
unsafe { ptr::write_bytes(filler.slot_data(s), cycle as u8, slot_bytes) };
// SAFETY: slot in range.
let meta = unsafe { &mut *filler.meta(s) };
meta.id = i64::from(cycle * 100 + s);
assert!(unsafe { filler.publish(s) });
published.push(s);
}
// Pool is full on the filler side.
let mut x = 0;
assert!(!unsafe { filler.acquire(&mut x) });
// Drain everything on the drainer side.
let mut consumed = Vec::new();
for _ in 0..slots {
let mut s = 0;
assert!(unsafe { drainer.consume(&mut s) });
// SAFETY: consumed slot.
let meta = unsafe { &*drainer.meta_const(s) };
assert_eq!(meta.id, i64::from(cycle * 100 + s));
// SAFETY: 1 byte readable.
assert_eq!(unsafe { *drainer.slot_data_const(s) }, cycle as u8);
consumed.push(s);
}
assert!(!unsafe { drainer.consume(&mut x) });
consumed.sort_unstable();
assert_eq!(consumed, vec![0, 1, 2]);
for s in consumed {
assert!(unsafe { drainer.release(s) });
}
}
}
#[test]
fn framepool_attach_rejects_wrong_magic() {
let key = test_key("pool-badmagic");
let size = FrameSlotPool::bytes_needed(2, 16);
let (owner, _peer) = two_mappings(&key, size);
// Overwrite the header area with garbage — no pool magic.
// SAFETY: owner mapping is live.
unsafe { ptr::write_bytes(owner.data(), 0xAB, 64) };
// SAFETY: buffer is live.
let pool = unsafe { FrameSlotPool::attach(owner.data()) };
assert!(!pool.is_valid());
assert_eq!(pool.slot_count(), 0);
assert_eq!(pool.slot_data_bytes(), 0);
}
#[test]
fn framepool_pool_over_reused_segment_is_consistent() {
// A pool that has been cycled fully and then attached fresh reports
// the same geometry as bytes_needed computed it.
let key = test_key("pool-geometry");
let slots = 5u32;
let slot_bytes = 1000usize;
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let _ = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
let attached = unsafe { FrameSlotPool::attach(peer.data()) };
assert!(attached.is_valid());
assert_eq!(attached.slot_count(), slots);
assert_eq!(attached.slot_data_bytes(), slot_bytes);
// Slot stride is 64-aligned (matches the C++ data layout).
// SAFETY: valid pool.
let s0 = unsafe { attached.slot_data(0) };
let s1 = unsafe { attached.slot_data(1) };
assert_eq!(s1 as usize - s0 as usize, align_up(slot_bytes, K_ALIGN));
}
// ---- SharedMemoryRegion ---------------------------------------------
#[test]
fn region_create_attach_write_visibility() {
let key = test_key("region-vis");
let size = 4096usize;
let (mut owner, mut peer) = two_mappings(&key, size);
assert!(owner.is_valid());
assert!(peer.is_valid());
assert_eq!(owner.size(), size);
assert_eq!(peer.size(), size);
assert_eq!(owner.key(), key);
assert_eq!(peer.key(), key);
// Owner writes; peer sees it through its own mapping.
// SAFETY: both mappings are live with `size` bytes.
unsafe {
let dst = owner.data() as *mut u32;
*dst = 0xDEADBEEF;
}
// SAFETY: peer mapping live.
let seen = unsafe { *(peer.data() as *const u32) };
assert_eq!(seen, 0xDEADBEEF);
// Peer writes back; owner sees it.
// SAFETY: peer mapping live.
unsafe {
let dst = peer.data() as *mut u32;
*dst = 0x12345678;
}
// SAFETY: owner mapping live.
assert_eq!(unsafe { *(owner.data() as *const u32) }, 0x12345678);
// Closing the ATTACH side does not unlink: while the owner lives,
// a third mapping can still open the name.
peer.close();
assert!(!peer.is_valid());
let mut third = SharedMemoryRegion::new();
assert!(third.open(&key, size, ShmMode::Attach), "{}", third.error());
assert!(third.is_valid());
third.close();
// Closing the OWNER unlinks the segment; further attaches fail.
owner.close();
assert!(!owner.is_valid());
let mut fourth = SharedMemoryRegion::new();
assert!(!fourth.open(&key, size, ShmMode::Attach));
}
#[test]
fn region_create_replaces_stale_segment() {
// Mirrors the C++: Create unlinks any stale segment with the same
// name first (crash cleanup), so a second Create SUCCEEDS and owns
// a fresh, zeroed segment.
let key = test_key("region-exists");
let size = 128usize;
let (mut owner, _peer) = two_mappings(&key, size);
assert!(owner.is_valid());
// SAFETY: owner mapping live.
unsafe { *(owner.data() as *mut u32) = 0xCAFEBABE };
let mut second = SharedMemoryRegion::new();
assert!(
second.open(&key, size, ShmMode::Create),
"{}",
second.error()
);
assert!(second.is_valid());
// The replacement segment is fresh (zeroed by create).
// SAFETY: second mapping live.
assert_eq!(unsafe { *(second.data() as *const u32) }, 0);
}
#[test]
fn region_attach_fails_when_segment_too_small() {
// macOS rounds shm segment sizes up to a 16 KiB minimum, so use
// sizes above that to exercise the size check.
let key = test_key("region-small");
let (owner, _peer) = two_mappings(&key, 4096);
assert!(owner.is_valid());
// Attaching with a larger size than the segment must fail (the
// fstat check, mirroring the C++).
let mut big = SharedMemoryRegion::new();
assert!(!big.open(&key, 65536, ShmMode::Attach));
assert!(!big.is_valid());
assert!(!big.error().is_empty());
}
#[test]
fn region_make_key_format() {
assert_eq!(SharedMemoryRegion::make_key(4242, 3), "olive-rw-4242-3");
assert_eq!(SharedMemoryRegion::make_key(1, 0), "olive-rw-1-0");
}
#[test]
fn region_keys_are_isolation_safe() {
// Keys with slashes are flattened to a single-slash POSIX name.
let key = "a/b/c";
let size = 64usize;
let (mut owner, mut peer) = two_mappings(key, size);
assert!(owner.is_valid());
assert!(peer.is_valid());
// The actual POSIX name is "/a_b_c".
// SAFETY: mapping live.
unsafe { *(owner.data() as *mut u32) = 7 };
// SAFETY: peer mapping live.
assert_eq!(unsafe { *(peer.data() as *const u32) }, 7);
}
}
// C ABI exports (engine/include/oakengine/ipc.h)
// ---------------------------------------------------------------------------
/// Opaque `OakSharedMemoryRegion` handle (ipc.h). A facade-owned box around
/// a [`SharedMemoryRegion`]; the C caller only ever sees the pointer.
#[repr(C)]
pub struct OakSharedMemoryRegion {
_opaque: [u8; 0],
}
/// Opaque `OakFrameSlotPool` handle (ipc.h). A facade-owned box around a
/// [`FrameSlotPool`] view.
#[repr(C)]
pub struct OakFrameSlotPool {
_opaque: [u8; 0],
}
/// `oakengine_ipc_shm_create` — allocate an empty (invalid) region object.
#[no_mangle]
pub extern "C" fn oakengine_ipc_shm_create() -> *mut OakSharedMemoryRegion {
guard_ptr(|| {
let region = Box::new(SharedMemoryRegion::new());
Ok(Box::into_raw(region) as *mut OakSharedMemoryRegion)
})
}
/// `oakengine_ipc_shm_free` — NULL no-op.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_shm_free(self_: *mut OakSharedMemoryRegion) {
if self_.is_null() {
return;
}
// SAFETY: `self_` was produced by `oakengine_ipc_shm_create` and is
// not used after this.
unsafe { drop(Box::from_raw(self_ as *mut SharedMemoryRegion)) };
}
/// `oakengine_ipc_shm_open` — open the segment; 1 on success, 0 on failure
/// (`oakengine_ipc_shm_error` carries the reason).
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_shm_open(
self_: *mut OakSharedMemoryRegion,
key: *const c_char,
size: usize,
mode: c_int,
) -> c_int {
crate::handle::guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let key = crate::handle::read_cstr(key);
let region = &mut *(self_ as *mut SharedMemoryRegion);
Ok(region.open(&key, size, ShmMode::from_c(mode)) as c_int)
})
}
/// `oakengine_ipc_shm_close` — unmap and (if owner) unlink.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_shm_close(self_: *mut OakSharedMemoryRegion) {
if self_.is_null() {
return;
}
// SAFETY: `self_` is a live region handle.
unsafe { (&mut *(self_ as *mut SharedMemoryRegion)).close() };
}
/// `oakengine_ipc_shm_is_valid` — 1/0.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_shm_is_valid(self_: *const OakSharedMemoryRegion) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
Ok((&*(self_ as *const SharedMemoryRegion)).is_valid() as c_int)
})
}
/// `oakengine_ipc_shm_data` — the mapped data pointer (NULL when invalid).
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_shm_data(self_: *mut OakSharedMemoryRegion) -> *mut c_void {
guard_ptr(|| unsafe {
if self_.is_null() {
return Ok(std::ptr::null_mut());
}
let region = &mut *(self_ as *mut SharedMemoryRegion);
Ok(region.data() as *mut c_void)
})
}
/// `oakengine_ipc_shm_size` — mapping size in bytes.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_shm_size(self_: *const OakSharedMemoryRegion) -> usize {
if self_.is_null() {
return 0;
}
// SAFETY: `self_` is a live region handle.
unsafe { (&*(self_ as *const SharedMemoryRegion)).size() }
}
/// `oakengine_ipc_shm_key` — the key the region was opened with
/// (buf/size convention).
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_shm_key(
self_: *const OakSharedMemoryRegion,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let region = &*(self_ as *const SharedMemoryRegion);
Ok(crate::handle::write_string(region.key(), buf, buf_size))
})
}
/// `oakengine_ipc_shm_error` — reason of the last failed open
/// (buf/size convention).
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_shm_error(
self_: *const OakSharedMemoryRegion,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let region = &*(self_ as *const SharedMemoryRegion);
Ok(crate::handle::write_string(region.error(), buf, buf_size))
})
}
/// `oakengine_ipc_shm_make_key` — build a unique segment key
/// ("olive-rw-<pid>-<index>", buf/size convention).
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_shm_make_key(
owner_pid: i64,
worker_index: c_int,
buf: *mut c_char,
buf_size: c_int,
) -> c_int {
guard_int(|| unsafe {
let key = SharedMemoryRegion::make_key(owner_pid, worker_index);
Ok(crate::handle::write_string(&key, buf, buf_size))
})
}
/// `oakengine_ipc_framepool_bytes_needed`.
#[no_mangle]
pub extern "C" fn oakengine_ipc_framepool_bytes_needed(
slot_count: u32,
slot_data_bytes: usize,
) -> usize {
FrameSlotPool::bytes_needed(slot_count, slot_data_bytes)
}
/// `oakengine_ipc_framepool_create` — lay out and initialize a brand-new
/// pool over `mem` (owner side, once). The handle is owned by the caller
/// but does not own `mem`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_create(
mem: *mut c_void,
slot_count: u32,
slot_data_bytes: usize,
) -> *mut OakFrameSlotPool {
guard_ptr(|| unsafe {
if mem.is_null() {
return Ok(std::ptr::null_mut());
}
let pool = FrameSlotPool::create(mem as *mut u8, slot_count, slot_data_bytes);
Ok(Box::into_raw(Box::new(pool)) as *mut OakFrameSlotPool)
})
}
/// `oakengine_ipc_framepool_attach` — map an existing pool (peer side).
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_attach(mem: *mut c_void) -> *mut OakFrameSlotPool {
guard_ptr(|| unsafe {
if mem.is_null() {
return Ok(std::ptr::null_mut());
}
let pool = FrameSlotPool::attach(mem as *mut u8);
if !pool.is_valid() {
return Ok(std::ptr::null_mut());
}
Ok(Box::into_raw(Box::new(pool)) as *mut OakFrameSlotPool)
})
}
/// `oakengine_ipc_framepool_copy` — copy the view (same shared memory,
/// independent handle). NULL yields NULL.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_copy(
self_: *const OakFrameSlotPool,
) -> *mut OakFrameSlotPool {
guard_ptr(|| unsafe {
if self_.is_null() {
return Ok(std::ptr::null_mut());
}
let pool = (*(self_ as *const FrameSlotPool)).clone();
Ok(Box::into_raw(Box::new(pool)) as *mut OakFrameSlotPool)
})
}
/// `oakengine_ipc_framepool_free` — NULL no-op.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_free(self_: *mut OakFrameSlotPool) {
if self_.is_null() {
return;
}
// SAFETY: `self_` was produced by one of the create/attach/copy
// exports and is not used after this.
unsafe { drop(Box::from_raw(self_ as *mut FrameSlotPool)) };
}
/// `oakengine_ipc_framepool_is_valid` — 1/0.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_is_valid(self_: *const OakFrameSlotPool) -> c_int {
guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
Ok((&*(self_ as *const FrameSlotPool)).is_valid() as c_int)
})
}
/// `oakengine_ipc_framepool_slot_count`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_slot_count(self_: *const OakFrameSlotPool) -> u32 {
if self_.is_null() {
return 0;
}
// SAFETY: `self_` is a live pool handle.
unsafe { (&*(self_ as *const FrameSlotPool)).slot_count() }
}
/// `oakengine_ipc_framepool_slot_data_bytes`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_slot_data_bytes(
self_: *const OakFrameSlotPool,
) -> usize {
if self_.is_null() {
return 0;
}
// SAFETY: `self_` is a live pool handle.
unsafe { (&*(self_ as *const FrameSlotPool)).slot_data_bytes() }
}
/// `oakengine_ipc_framepool_acquire` — take a free slot; 1 on success
/// (`*index` set), 0 if none is free or the pool is invalid.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_acquire(
self_: *mut OakFrameSlotPool,
index: *mut u32,
) -> c_int {
crate::handle::guard_int(|| unsafe {
if self_.is_null() || index.is_null() {
return Ok(0);
}
let pool = &*(self_ as *const FrameSlotPool);
if !pool.is_valid() {
return Ok(0);
}
let mut out = 0u32;
if pool.acquire(&mut out) {
*index = out;
Ok(1)
} else {
Ok(0)
}
})
}
/// `oakengine_ipc_framepool_slot_data` — pointer to a slot's pixel block.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_slot_data(
self_: *mut OakFrameSlotPool,
index: u32,
) -> *mut c_void {
guard_ptr(|| unsafe {
if self_.is_null() {
return Ok(std::ptr::null_mut());
}
let pool = &*(self_ as *const FrameSlotPool);
if !pool.is_valid() || index >= pool.slot_count() {
return Ok(std::ptr::null_mut());
}
Ok(pool.slot_data(index) as *mut c_void)
})
}
/// `oakengine_ipc_framepool_slot_data_const`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_slot_data_const(
self_: *const OakFrameSlotPool,
index: u32,
) -> *const c_void {
guard_ptr(|| unsafe {
if self_.is_null() {
return Ok(std::ptr::null_mut());
}
let pool = &*(self_ as *const FrameSlotPool);
if !pool.is_valid() || index >= pool.slot_count() {
return Ok(std::ptr::null_mut());
}
Ok(pool.slot_data_const(index) as *mut c_void)
})
}
/// `oakengine_ipc_framepool_meta` — mutable per-slot metadata (borrowed).
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_meta(
self_: *mut OakFrameSlotPool,
index: u32,
) -> *mut crate::ipc::FrameSlotMeta {
guard_ptr(|| unsafe {
if self_.is_null() {
return Ok(std::ptr::null_mut());
}
let pool = &*(self_ as *const FrameSlotPool);
if !pool.is_valid() || index >= pool.slot_count() {
return Ok(std::ptr::null_mut());
}
Ok(pool.meta(index))
})
}
/// `oakengine_ipc_framepool_meta_const`.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_meta_const(
self_: *const OakFrameSlotPool,
index: u32,
) -> *const crate::ipc::FrameSlotMeta {
guard_ptr(|| unsafe {
if self_.is_null() {
return Ok(std::ptr::null_mut());
}
let pool = &*(self_ as *const FrameSlotPool);
if !pool.is_valid() || index >= pool.slot_count() {
return Ok(std::ptr::null_mut());
}
Ok(pool.meta_const(index) as *mut FrameSlotMeta)
})
}
/// `oakengine_ipc_framepool_publish` — publish a filled slot; 1 on success,
/// 0 if the ready ring is full or the pool is invalid.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_publish(
self_: *mut OakFrameSlotPool,
index: u32,
) -> c_int {
crate::handle::guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let pool = &*(self_ as *const FrameSlotPool);
if !pool.is_valid() || index >= pool.slot_count() {
return Ok(0);
}
Ok(pool.publish(index) as c_int)
})
}
/// `oakengine_ipc_framepool_consume` — take the next published slot; 1 on
/// success (`*index` set), 0 if nothing is ready or the pool is invalid.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_consume(
self_: *mut OakFrameSlotPool,
index: *mut u32,
) -> c_int {
crate::handle::guard_int(|| unsafe {
if self_.is_null() || index.is_null() {
return Ok(0);
}
let pool = &*(self_ as *const FrameSlotPool);
if !pool.is_valid() {
return Ok(0);
}
let mut out = 0u32;
if pool.consume(&mut out) {
*index = out;
Ok(1)
} else {
Ok(0)
}
})
}
/// `oakengine_ipc_framepool_release` — return a consumed slot to the free
/// pool; 1 on success, 0 if the free ring is full or the pool is invalid.
#[no_mangle]
pub unsafe extern "C" fn oakengine_ipc_framepool_release(
self_: *mut OakFrameSlotPool,
index: u32,
) -> c_int {
crate::handle::guard_int(|| unsafe {
if self_.is_null() {
return Ok(0);
}
let pool = &*(self_ as *const FrameSlotPool);
if !pool.is_valid() || index >= pool.slot_count() {
return Ok(0);
}
Ok(pool.release(index) as c_int)
})
}
// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod cabi_tests {
use super::*;
/// A unique, temporary POSIX segment key for a test (pid + counter), so
/// parallel test runs never collide.
fn test_key(name: &str) -> String {
static COUNTER: AtomicU32 = AtomicU32::new(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
SharedMemoryRegion::make_key(i64::from(std::process::id()), (n & 0x7FFF) as i32)
+ &format!("-{name}")
}
/// Create one segment and map it a second time — the in-process
/// equivalent of two processes sharing a segment. Returns
/// `(owner_region, peer_region)`; both must be kept alive for the
/// whole test (the peer is an attach that does not unlink).
fn two_mappings(key: &str, size: usize) -> (SharedMemoryRegion, SharedMemoryRegion) {
let mut owner = SharedMemoryRegion::new();
assert!(
owner.open(key, size, ShmMode::Create),
"create failed: {}",
owner.error()
);
let mut peer = SharedMemoryRegion::new();
assert!(
peer.open(key, size, ShmMode::Attach),
"attach failed: {}",
peer.error()
);
(owner, peer)
}
// ---- SpscRingBuffer -------------------------------------------------
#[test]
fn ring_bytes_needed_matches_cpp_layout() {
// 12 header bytes + capacity * 4.
assert_eq!(SpscRingBuffer::bytes_needed(4), 12 + 16);
assert_eq!(SpscRingBuffer::bytes_needed(5), 12 + 20);
assert_eq!(SpscRingBuffer::bytes_needed(0), 12);
}
#[test]
fn ring_empty_full_and_single_entry() {
let key = test_key("ring-empty");
let size = SpscRingBuffer::bytes_needed(4);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: both mappings are live and at least `size` bytes.
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
assert!(unsafe { cons.is_empty_approx() });
let mut v = 99;
assert!(!unsafe { cons.pop(&mut v) });
assert_eq!(v, 99);
assert!(unsafe { prod.push(7) });
assert!(!unsafe { cons.is_empty_approx() });
assert_eq!(unsafe { cons.size_approx() }, 1);
assert!(unsafe { cons.pop(&mut v) });
assert_eq!(v, 7);
assert!(unsafe { cons.is_empty_approx() });
}
#[test]
fn ring_capacity_minus_one_live_entries() {
// A ring of capacity N holds at most N-1 entries (one slot is
// always left empty to tell full from empty).
let key = test_key("ring-cap");
let size = SpscRingBuffer::bytes_needed(4);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
for i in 0..3 {
assert!(unsafe { prod.push(i) });
}
// The 4th push must fail: head would collide with tail.
assert!(!unsafe { prod.push(99) });
let mut v = 0;
for expected in 0..3 {
assert!(unsafe { cons.pop(&mut v) });
assert_eq!(v, expected);
}
assert!(!unsafe { cons.pop(&mut v) });
}
#[test]
fn ring_wraparound_preserves_order() {
// Fill, drain, then wrap past the end of the slot array: cursors
// are modulo-capacity, order must be preserved across the wrap.
let key = test_key("ring-wrap");
let size = SpscRingBuffer::bytes_needed(4);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let prod = unsafe { SpscRingBuffer::create(owner.data(), 4) };
let cons = unsafe { SpscRingBuffer::attach(peer.data()) };
for i in 0..3 {
assert!(unsafe { prod.push(i) });
}
let mut v = 0;
for _ in 0..3 {
assert!(unsafe { cons.pop(&mut v) });
}
// Ring is empty again; push past the wrap point.
for i in 3..6 {
assert!(unsafe { prod.push(i) });
}
for expected in 3..6 {
assert!(unsafe { cons.pop(&mut v) });
assert_eq!(v, expected);
}
}
// ---- FrameSlotPool --------------------------------------------------
#[test]
fn framepool_bytes_needed_matches_cpp_offsets() {
// Recompute by hand with the C++ layout: header 64, each ring
// align_up(12 + 4*(n+1), 64), meta align_up(176*n, 64), data
// align_up(slot_bytes, 64) * n.
let check = |n: u32, slot: usize| {
let ring = align_up(12 + 4 * (n as usize + 1), 64);
let expected =
64 + ring + ring + align_up(176 * n as usize, 64) + align_up(slot, 64) * n as usize;
assert_eq!(FrameSlotPool::bytes_needed(n, slot), expected);
};
check(4, 4096);
check(6, 1_000_000);
check(1, 64);
check(3, 100);
}
#[test]
fn framepool_create_attach_two_processes_both_directions() {
// "Two processes": two mappings of the same segment. Owner creates
// the pool; the peer attaches. A filler on one side and a drainer
// on the other exchange slots in both directions.
let key = test_key("pool-bidi");
let slots = 4u32;
let slot_bytes = 64usize;
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: both mappings are live and sized by bytes_needed.
let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
let drainer = unsafe { FrameSlotPool::attach(peer.data()) };
assert!(filler.is_valid());
assert!(drainer.is_valid());
assert_eq!(drainer.slot_count(), slots);
assert_eq!(drainer.slot_data_bytes(), slot_bytes);
// Filler acquires every slot exactly once (seeded free ring), then
// the free ring is empty.
let mut got = Vec::new();
for _ in 0..slots {
let mut s = 0;
assert!(unsafe { filler.acquire(&mut s) });
got.push(s);
}
got.sort_unstable();
assert_eq!(got, vec![0, 1, 2, 3]);
let mut extra = 0;
assert!(!unsafe { filler.acquire(&mut extra) });
// Drainer sees nothing ready yet.
assert!(!unsafe { drainer.consume(&mut extra) });
// Filler writes pixels + meta into two slots and publishes them.
for (i, slot) in [0u32, 2u32].iter().enumerate() {
// SAFETY: `slot` was acquired above.
let data = unsafe { filler.slot_data(*slot) };
unsafe { ptr::write_bytes(data, (i * 40 + 1) as u8, slot_bytes) };
// SAFETY: slot in range.
let meta = unsafe { &mut *filler.meta(*slot) };
meta.id = 100 + *slot as i64;
meta.width = 8;
meta.height = 8;
meta.data_size = slot_bytes as i32;
assert!(unsafe { filler.publish(*slot) });
}
// Drainer consumes them through its own mapping and sees the same
// payloads and metadata.
let mut consumed = Vec::new();
for _ in 0..2 {
let mut s = 0;
assert!(unsafe { drainer.consume(&mut s) });
// SAFETY: s was consumed.
let data = unsafe { drainer.slot_data_const(s) };
let meta = unsafe { &*drainer.meta_const(s) };
assert_eq!(meta.id, 100 + s as i64);
assert_eq!(meta.width, 8);
assert_eq!(meta.data_size, slot_bytes as i32);
// SAFETY: slot_bytes readable in the slot block.
let first = unsafe { *data };
assert_eq!(first, ((s as usize / 2) * 40 + 1) as u8);
consumed.push(s);
}
consumed.sort_unstable();
assert_eq!(consumed, vec![0, 2]);
assert!(!unsafe { drainer.consume(&mut extra) });
// Drainer releases the slots back; the filler can acquire them
// again — the full round trip through both rings.
for s in consumed {
assert!(unsafe { drainer.release(s) });
}
let mut s = 0;
assert!(unsafe { filler.acquire(&mut s) });
assert_eq!(s, 0);
}
#[test]
fn framepool_wraparound_and_full_edges() {
// Small pool: cycle every slot many times, verifying the rings'
// modulo behavior end to end.
let key = test_key("pool-wrap");
let slots = 3u32;
let slot_bytes = 32usize;
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let filler = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
let drainer = unsafe { FrameSlotPool::attach(peer.data()) };
for cycle in 0..4u32 {
let mut published = Vec::new();
for _ in 0..slots {
let mut s = 0;
assert!(unsafe { filler.acquire(&mut s) }, "cycle {cycle}");
// SAFETY: acquired slot.
unsafe { ptr::write_bytes(filler.slot_data(s), cycle as u8, slot_bytes) };
// SAFETY: slot in range.
let meta = unsafe { &mut *filler.meta(s) };
meta.id = i64::from(cycle * 100 + s);
assert!(unsafe { filler.publish(s) });
published.push(s);
}
// Pool is full on the filler side.
let mut x = 0;
assert!(!unsafe { filler.acquire(&mut x) });
// Drain everything on the drainer side.
let mut consumed = Vec::new();
for _ in 0..slots {
let mut s = 0;
assert!(unsafe { drainer.consume(&mut s) });
// SAFETY: consumed slot.
let meta = unsafe { &*drainer.meta_const(s) };
assert_eq!(meta.id, i64::from(cycle * 100 + s));
// SAFETY: 1 byte readable.
assert_eq!(unsafe { *drainer.slot_data_const(s) }, cycle as u8);
consumed.push(s);
}
assert!(!unsafe { drainer.consume(&mut x) });
consumed.sort_unstable();
assert_eq!(consumed, vec![0, 1, 2]);
for s in consumed {
assert!(unsafe { drainer.release(s) });
}
}
}
#[test]
fn framepool_attach_rejects_wrong_magic() {
let key = test_key("pool-badmagic");
let size = FrameSlotPool::bytes_needed(2, 16);
let (owner, _peer) = two_mappings(&key, size);
// Overwrite the header area with garbage — no pool magic.
// SAFETY: owner mapping is live.
unsafe { ptr::write_bytes(owner.data(), 0xAB, 64) };
// SAFETY: buffer is live.
let pool = unsafe { FrameSlotPool::attach(owner.data()) };
assert!(!pool.is_valid());
assert_eq!(pool.slot_count(), 0);
assert_eq!(pool.slot_data_bytes(), 0);
}
#[test]
fn framepool_pool_over_reused_segment_is_consistent() {
// A pool that has been cycled fully and then attached fresh reports
// the same geometry as bytes_needed computed it.
let key = test_key("pool-geometry");
let slots = 5u32;
let slot_bytes = 1000usize;
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
let (owner, peer) = two_mappings(&key, size);
// SAFETY: live mappings.
let _ = unsafe { FrameSlotPool::create(owner.data(), slots, slot_bytes) };
let attached = unsafe { FrameSlotPool::attach(peer.data()) };
assert!(attached.is_valid());
assert_eq!(attached.slot_count(), slots);
assert_eq!(attached.slot_data_bytes(), slot_bytes);
// Slot stride is 64-aligned (matches the C++ data layout).
// SAFETY: valid pool.
let s0 = unsafe { attached.slot_data(0) };
let s1 = unsafe { attached.slot_data(1) };
assert_eq!(s1 as usize - s0 as usize, align_up(slot_bytes, K_ALIGN));
}
// ---- SharedMemoryRegion ---------------------------------------------
#[test]
fn region_create_attach_write_visibility() {
let key = test_key("region-vis");
let size = 4096usize;
let (mut owner, mut peer) = two_mappings(&key, size);
assert!(owner.is_valid());
assert!(peer.is_valid());
assert_eq!(owner.size(), size);
assert_eq!(peer.size(), size);
assert_eq!(owner.key(), key);
assert_eq!(peer.key(), key);
// Owner writes; peer sees it through its own mapping.
// SAFETY: both mappings are live with `size` bytes.
unsafe {
let dst = owner.data() as *mut u32;
*dst = 0xDEADBEEF;
}
// SAFETY: peer mapping live.
let seen = unsafe { *(peer.data() as *const u32) };
assert_eq!(seen, 0xDEADBEEF);
// Peer writes back; owner sees it.
// SAFETY: peer mapping live.
unsafe {
let dst = peer.data() as *mut u32;
*dst = 0x12345678;
}
// SAFETY: owner mapping live.
assert_eq!(unsafe { *(owner.data() as *const u32) }, 0x12345678);
// Closing the ATTACH side does not unlink: while the owner lives,
// a third mapping can still open the name.
peer.close();
assert!(!peer.is_valid());
let mut third = SharedMemoryRegion::new();
assert!(third.open(&key, size, ShmMode::Attach), "{}", third.error());
assert!(third.is_valid());
third.close();
// Closing the OWNER unlinks the segment; further attaches fail.
owner.close();
assert!(!owner.is_valid());
let mut fourth = SharedMemoryRegion::new();
assert!(!fourth.open(&key, size, ShmMode::Attach));
}
#[test]
fn region_create_replaces_stale_segment() {
// Mirrors the C++: Create unlinks any stale segment with the same
// name first (crash cleanup), so a second Create SUCCEEDS and owns
// a fresh, zeroed segment.
let key = test_key("region-exists");
let size = 128usize;
let (mut owner, _peer) = two_mappings(&key, size);
assert!(owner.is_valid());
// SAFETY: owner mapping live.
unsafe { *(owner.data() as *mut u32) = 0xCAFEBABE };
let mut second = SharedMemoryRegion::new();
assert!(
second.open(&key, size, ShmMode::Create),
"{}",
second.error()
);
assert!(second.is_valid());
// The replacement segment is fresh (zeroed by create).
// SAFETY: second mapping live.
assert_eq!(unsafe { *(second.data() as *const u32) }, 0);
}
#[test]
fn region_attach_fails_when_segment_too_small() {
// macOS rounds shm segment sizes up to a 16 KiB minimum, so use
// sizes above that to exercise the size check.
let key = test_key("region-small");
let (owner, _peer) = two_mappings(&key, 4096);
assert!(owner.is_valid());
// Attaching with a larger size than the segment must fail (the
// fstat check, mirroring the C++).
let mut big = SharedMemoryRegion::new();
assert!(!big.open(&key, 65536, ShmMode::Attach));
assert!(!big.is_valid());
assert!(!big.error().is_empty());
}
#[test]
fn region_make_key_format() {
assert_eq!(SharedMemoryRegion::make_key(4242, 3), "olive-rw-4242-3");
assert_eq!(SharedMemoryRegion::make_key(1, 0), "olive-rw-1-0");
}
#[test]
fn region_keys_are_isolation_safe() {
// Keys with slashes are flattened to a single-slash POSIX name.
let key = "a/b/c";
let size = 64usize;
let (mut owner, mut peer) = two_mappings(key, size);
assert!(owner.is_valid());
assert!(peer.is_valid());
// The actual POSIX name is "/a_b_c".
// SAFETY: mapping live.
unsafe { *(owner.data() as *mut u32) = 7 };
// SAFETY: peer mapping live.
assert_eq!(unsafe { *(peer.data() as *const u32) }, 7);
}
// ---- C ABI wrappers ---------------------------------------------------
#[test]
fn c_abi_shm_make_key_buf_size() {
let mut buf = [0 as c_char; 64];
let n = unsafe { oakengine_ipc_shm_make_key(7, 2, buf.as_mut_ptr(), 64) };
assert_eq!(n, "olive-rw-7-2".len() as c_int);
let s = unsafe { std::ffi::CStr::from_ptr(buf.as_ptr()) }
.to_string_lossy()
.into_owned();
assert_eq!(s, "olive-rw-7-2");
// NULL/0 buffer only queries the size.
assert_eq!(
unsafe { oakengine_ipc_shm_make_key(7, 2, std::ptr::null_mut(), 0) },
n
);
}
#[test]
fn c_abi_framepool_create_attach_publish_consume() {
let key = test_key("cabi-pool");
let slots = 2u32;
let slot_bytes = 32usize;
let size = FrameSlotPool::bytes_needed(slots, slot_bytes);
let (mut owner, mut peer) = two_mappings(&key, size);
// SAFETY: both regions live.
let owner_ptr = unsafe { owner.data() as *mut c_void };
let peer_ptr = unsafe { peer.data() as *mut c_void };
// SAFETY: buffers sized by bytes_needed.
let pool = unsafe { oakengine_ipc_framepool_create(owner_ptr, slots, slot_bytes) };
assert!(!pool.is_null());
assert_eq!(unsafe { oakengine_ipc_framepool_is_valid(pool) }, 1);
assert_eq!(unsafe { oakengine_ipc_framepool_slot_count(pool) }, slots);
assert_eq!(
unsafe { oakengine_ipc_framepool_slot_data_bytes(pool) },
slot_bytes
);
// SAFETY: peer buffer is a live mapping of the same segment.
let attached = unsafe { oakengine_ipc_framepool_attach(peer_ptr) };
assert!(!attached.is_null());
assert_eq!(unsafe { oakengine_ipc_framepool_is_valid(attached) }, 1);
let mut slot = 0u32;
assert_eq!(
unsafe { oakengine_ipc_framepool_acquire(pool, &mut slot) },
1
);
// SAFETY: acquired slot; slot_bytes writable (count in bytes).
unsafe {
ptr::write_bytes(
oakengine_ipc_framepool_slot_data(pool, slot) as *mut u8,
0x5A,
slot_bytes,
);
}
// SAFETY: slot in range.
let meta = unsafe { &mut *oakengine_ipc_framepool_meta(pool, slot) };
meta.id = 77;
assert_eq!(unsafe { oakengine_ipc_framepool_publish(pool, slot) }, 1);
let mut got = 99u32;
assert_eq!(
unsafe { oakengine_ipc_framepool_consume(attached, &mut got) },
1
);
assert_eq!(got, slot);
// SAFETY: consumed slot.
let meta = unsafe { &*oakengine_ipc_framepool_meta_const(attached, got) };
assert_eq!(meta.id, 77);
// SAFETY: slot_bytes readable.
assert_eq!(
unsafe { *(oakengine_ipc_framepool_slot_data_const(attached, got) as *const u8) },
0x5A
);
assert_eq!(unsafe { oakengine_ipc_framepool_release(attached, got) }, 1);
unsafe { oakengine_ipc_framepool_free(pool) };
unsafe { oakengine_ipc_framepool_free(attached) };
}
#[test]
fn c_abi_shm_open_data_size_error() {
let key = test_key("cabi-shm");
let size = 256usize;
// SAFETY: shm_create returns an owned handle.
let region = unsafe { oakengine_ipc_shm_create() };
assert!(!region.is_null());
let key_c = std::ffi::CString::new(key.clone()).unwrap();
// SAFETY: valid C string + owned handle.
let rc = unsafe { oakengine_ipc_shm_open(region, key_c.as_ptr(), size, 0) };
assert_eq!(rc, 1);
assert_eq!(unsafe { oakengine_ipc_shm_is_valid(region) }, 1);
assert_eq!(unsafe { oakengine_ipc_shm_size(region) }, size);
// SAFETY: mapping live.
assert!(!unsafe { oakengine_ipc_shm_data(region) }.is_null());
let mut kb = [0 as c_char; 128];
let n = unsafe { oakengine_ipc_shm_key(region, kb.as_mut_ptr(), 128) };
assert_eq!(n, key.len() as c_int);
assert_eq!(unsafe { oakengine_ipc_shm_is_valid(region) }, 1);
unsafe { oakengine_ipc_shm_close(region) };
assert_eq!(unsafe { oakengine_ipc_shm_is_valid(region) }, 0);
unsafe { oakengine_ipc_shm_free(region) };
// A failed open records a human-readable error.
// SAFETY: shm_create returns an owned handle.
let region2 = unsafe { oakengine_ipc_shm_create() };
let bad = std::ffi::CString::new("olive-rw-no-such-segment-for-test").unwrap();
// SAFETY: valid C string + owned handle.
assert_eq!(
unsafe { oakengine_ipc_shm_open(region2, bad.as_ptr(), size, 1) },
0
);
let mut eb = [0 as c_char; 256];
let n = unsafe { oakengine_ipc_shm_error(region2, eb.as_mut_ptr(), 256) };
assert!(n > 0);
assert_eq!(unsafe { oakengine_ipc_shm_is_valid(region2) }, 0);
unsafe { oakengine_ipc_shm_free(region2) };
}
}