Files
oak-editor/engine/include/oakengine/ipc.h
T
Mike-Solar 37845302f9 engine: begin the liboakengine C ABI facade with the IPC subsystem
- oakengine/export.h establishes the OAKENGINE_API visibility macros;
  include/oakengine/ipc.h is the first pure-C surface (41 functions:
  shm, frame slot pool, and the worker IPC messages as POD<->JSON
  build/parse), implemented in engine/src/capi/
- the IPC implementations move to engine/src/oliveimpl (namespace
  olive::engine::internal::ipc); engine/render/ipc/*.h are rebuilt as
  same-name/same-API wrapper classes forwarding across the C boundary
- FrameSlotMeta is shared with the C header verbatim so the app/worker
  wire format (v1) is bit-identical; static_asserts pin sizeof and
  field offsets
- spscringbuffer.h moves to include/oakengine/ as an inline-only
  header (no symbols, not ABI)
- new pure-C test oakengine_ipc_test (make_oakengine_test, no GL)
  covers shm, frame pool, message round-trips and the layout asserts;
  full gtest suite stays green (1986 tests)
2026-07-20 04:12:58 +08:00

432 lines
16 KiB
C

/***
Oak - 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/>.
***/
#ifndef OAKENGINE_IPC_H
#define OAKENGINE_IPC_H
#include <stddef.h>
#include <stdint.h>
#include "export.h"
#ifdef __cplusplus
extern "C" {
#endif
/**
* @file ipc.h
* @brief C ABI for the render worker IPC subsystem
*
* Two channels are covered:
*
* - Bulk data: named shared-memory segments (OakSharedMemoryRegion) holding
* a fixed-size pool of frame slots (OakFrameSlotPool). The in-memory
* layout produced by oakengine_ipc_framepool_create() is the wire
* protocol (version 1) shared with the render worker binary and never
* changes. oak_frame_slot_meta is the POD metadata record that lives in
* that shared layout, so it is exposed here as a plain C struct.
*
* - Control plane: newline-delimited JSON messages exchanged over stdio
* pipes. No Qt types cross this boundary; each message type has a POD
* struct plus build/parse functions converting between the struct and a
* compact JSON string.
*
* Conventions:
* - Returned Oak* handles are owned by the caller and must be released
* with the matching _free(). NULL is accepted by every function and
* yields a no-op / zero result.
* - Booleans are int (1/0). Fallible parses return 1 on success, 0 on
* failure.
* - String output uses the buf/size convention: the return value is the
* number of characters that would have been written excluding the NUL,
* so buf == NULL or a short buffer queries the required size. The
* output is NUL-terminated whenever buf_size > 0.
* - POD message structs carry strings in fixed-capacity inline buffers so
* they stay trivially copyable; overlong input is truncated at the
* capacity.
*/
/** @brief Capacity of oak_frame_slot_meta::colorspace, including the NUL. */
#define OAK_IPC_COLORSPACE_CAP 128
/** @brief Capacity of shared-memory key string fields, including the NUL. */
#define OAK_IPC_SHM_KEY_CAP 128
/** @brief Capacity of oak_ipc_render_frame::node_uuid, including the NUL. */
#define OAK_IPC_NODE_UUID_CAP 64
/** @brief Capacity of the color transform name fields, including the NUL. */
#define OAK_IPC_COLOR_STR_CAP 128
/** @brief Capacity of oak_ipc_load_graph::path, including the NUL. */
#define OAK_IPC_PATH_CAP 1024
/** @brief Maximum number of decoded input slots carried by one render_frame. */
#define OAK_IPC_INPUT_SLOTS_CAP 64
/** @brief Capacity of the message buffer filled by oakengine_ipc_error_parse. */
#define OAK_IPC_ERROR_MESSAGE_CAP 512
/**
* @brief Message type strings on the control-plane wire format.
*
* Every control message is a compact JSON object on one line whose "type"
* field carries one of these values. graph_update is reserved (no payload
* struct is defined yet).
*/
#define OAKENGINE_IPC_MSGTYPE_HANDSHAKE "handshake"
#define OAKENGINE_IPC_MSGTYPE_LOAD_GRAPH "load_graph"
#define OAKENGINE_IPC_MSGTYPE_RENDER_FRAME "render_frame"
#define OAKENGINE_IPC_MSGTYPE_FRAME_READY "frame_ready"
#define OAKENGINE_IPC_MSGTYPE_CANCEL "cancel"
#define OAKENGINE_IPC_MSGTYPE_GRAPH_UPDATE "graph_update"
#define OAKENGINE_IPC_MSGTYPE_SHUTDOWN "shutdown"
#define OAKENGINE_IPC_MSGTYPE_ERROR "error"
/**
* @brief Message type discriminator mirroring the msgtype strings.
*
* Same-named values for each wire message type; returned by
* oakengine_ipc_message_type() so a C consumer can dispatch an incoming JSON
* line without hard-coding the strings.
*/
typedef enum oak_ipc_msgtype {
OAK_IPC_MSGTYPE_UNKNOWN = -1,
OAK_IPC_MSGTYPE_HANDSHAKE = 0,
OAK_IPC_MSGTYPE_LOAD_GRAPH,
OAK_IPC_MSGTYPE_RENDER_FRAME,
OAK_IPC_MSGTYPE_FRAME_READY,
OAK_IPC_MSGTYPE_CANCEL,
OAK_IPC_MSGTYPE_GRAPH_UPDATE,
OAK_IPC_MSGTYPE_SHUTDOWN,
OAK_IPC_MSGTYPE_ERROR
} oak_ipc_msgtype;
/**
* @brief Open mode for oakengine_ipc_shm_open().
*/
typedef enum oak_ipc_shm_mode {
/** Create (and own) the segment. Fails if it exists; unlinks on close. */
OAK_IPC_SHM_MODE_CREATE = 0,
/** Attach to a segment created by the peer. Does not unlink on close. */
OAK_IPC_SHM_MODE_ATTACH = 1
} oak_ipc_shm_mode;
/**
* @brief Per-slot metadata describing the frame currently occupying a slot.
*
* Trivially-copyable POD that lives in shared memory alongside the pixel
* data, part of the version-1 wire protocol between the app and the render
* worker. Carries everything the consumer needs to reconstruct a frame
* without any out-of-band information. The timestamp is stored as an
* explicit numerator/denominator pair to stay POD.
*/
typedef struct oak_frame_slot_meta {
int64_t id; /**< Caller-defined tag (e.g. ticket id, or footage stream hash). */
int64_t time_num; /**< Frame timestamp numerator. */
int64_t time_den; /**< Frame timestamp denominator. */
int32_t width;
int32_t height;
int32_t format; /**< PixelFormat::Format value. */
int32_t channel_count;
int32_t linesize; /**< Bytes per scanline (stride). */
int32_t data_size; /**< Valid bytes written into the slot's data block. */
char colorspace[OAK_IPC_COLORSPACE_CAP]; /**< Input colorspace name. */
} oak_frame_slot_meta;
typedef struct OakSharedMemoryRegion OakSharedMemoryRegion;
typedef struct OakFrameSlotPool OakFrameSlotPool;
/* ---- SharedMemoryRegion ------------------------------------------------- */
/**
* @brief Allocate an empty (invalid) region object. Owned by the caller.
*/
OAKENGINE_API OakSharedMemoryRegion *oakengine_ipc_shm_create(void);
OAKENGINE_API void oakengine_ipc_shm_free(OakSharedMemoryRegion *self);
/**
* @brief 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 1 on success; on failure returns 0 and
* oakengine_ipc_shm_error() carries a human-readable reason.
*/
OAKENGINE_API int oakengine_ipc_shm_open(OakSharedMemoryRegion *self,
const char *key, size_t size,
oak_ipc_shm_mode mode);
/**
* @brief Unmap and (if owner) unlink the segment. Also done by _free().
*/
OAKENGINE_API void oakengine_ipc_shm_close(OakSharedMemoryRegion *self);
OAKENGINE_API int oakengine_ipc_shm_is_valid(const OakSharedMemoryRegion *self);
OAKENGINE_API void *oakengine_ipc_shm_data(OakSharedMemoryRegion *self);
OAKENGINE_API size_t oakengine_ipc_shm_size(const OakSharedMemoryRegion *self);
/**
* @brief The key the region was opened with (buf/size convention).
*/
OAKENGINE_API int oakengine_ipc_shm_key(const OakSharedMemoryRegion *self,
char *buf, int buf_size);
/**
* @brief Human-readable reason for the last failed open (buf/size convention).
*/
OAKENGINE_API int oakengine_ipc_shm_error(const OakSharedMemoryRegion *self,
char *buf, int buf_size);
/**
* @brief Build a unique segment key for a worker, e.g. "olive-rw-<pid>-<index>".
*
* Centralized so the owner and the spawned worker agree on the same name.
* Uses the buf/size convention.
*/
OAKENGINE_API int oakengine_ipc_shm_make_key(int64_t owner_pid,
int worker_index, char *buf,
int buf_size);
/* ---- FrameSlotPool ------------------------------------------------------ */
/**
* @brief Total bytes a region must provide to back a pool of `slot_count` x
* `slot_data_bytes`.
*/
OAKENGINE_API size_t
oakengine_ipc_framepool_bytes_needed(uint32_t slot_count,
size_t slot_data_bytes);
/**
* @brief Lay out and initialize a brand-new pool over `mem` (owner side, once).
*
* `mem` must provide at least oakengine_ipc_framepool_bytes_needed() bytes
* and must outlive the returned handle. The handle is owned by the caller;
* it does not own `mem`.
*/
OAKENGINE_API OakFrameSlotPool *
oakengine_ipc_framepool_create(void *mem, uint32_t slot_count,
size_t slot_data_bytes);
/**
* @brief Map an existing, already-initialized pool (peer side).
*
* Reads the geometry from the in-memory header written by _create(); the
* handle reports is_valid() == 0 if the magic does not match.
*/
OAKENGINE_API OakFrameSlotPool *oakengine_ipc_framepool_attach(void *mem);
/**
* @brief Copy the view (same shared memory, independent handle). Owned.
*/
OAKENGINE_API OakFrameSlotPool *
oakengine_ipc_framepool_copy(const OakFrameSlotPool *self);
OAKENGINE_API void oakengine_ipc_framepool_free(OakFrameSlotPool *self);
OAKENGINE_API int oakengine_ipc_framepool_is_valid(const OakFrameSlotPool *self);
OAKENGINE_API uint32_t
oakengine_ipc_framepool_slot_count(const OakFrameSlotPool *self);
OAKENGINE_API size_t
oakengine_ipc_framepool_slot_data_bytes(const OakFrameSlotPool *self);
/* Filler side: acquire a free slot, write meta + pixels, publish it. */
/**
* @brief Take ownership of a free slot. Returns 0 (leaving *index untouched)
* if none is free.
*/
OAKENGINE_API int oakengine_ipc_framepool_acquire(OakFrameSlotPool *self,
uint32_t *index);
/**
* @brief Pointer to a slot's pixel data block (slot_data_bytes available).
*/
OAKENGINE_API void *oakengine_ipc_framepool_slot_data(OakFrameSlotPool *self,
uint32_t index);
OAKENGINE_API const void *
oakengine_ipc_framepool_slot_data_const(const OakFrameSlotPool *self,
uint32_t index);
/**
* @brief Mutable metadata for a slot. The filler writes this before publish.
* The returned pointer addresses shared memory; it is borrowed, not owned.
*/
OAKENGINE_API oak_frame_slot_meta *
oakengine_ipc_framepool_meta(OakFrameSlotPool *self, uint32_t index);
OAKENGINE_API const oak_frame_slot_meta *
oakengine_ipc_framepool_meta_const(const OakFrameSlotPool *self,
uint32_t index);
/**
* @brief Publish a filled slot to the drainer. Must follow a successful
* acquire of `index`.
*/
OAKENGINE_API int oakengine_ipc_framepool_publish(OakFrameSlotPool *self,
uint32_t index);
/* Drainer side: consume the next published slot, read it, release it. */
/**
* @brief Take the next published slot. Returns 0 if nothing is ready.
*/
OAKENGINE_API int oakengine_ipc_framepool_consume(OakFrameSlotPool *self,
uint32_t *index);
/**
* @brief Return a consumed slot to the free pool. Must follow a consume of
* `index`.
*/
OAKENGINE_API int oakengine_ipc_framepool_release(OakFrameSlotPool *self,
uint32_t index);
/* ---- Control-plane messages --------------------------------------------- */
/**
* @brief Negotiate protocol version and announce shared-memory key/geometry.
*
* Field-for-field equivalent of the C++ HandshakeMsg; strings are inline
* buffers with the documented capacities.
*/
typedef struct oak_ipc_handshake {
int32_t protocol_version;
char shm_key[OAK_IPC_SHM_KEY_CAP]; /**< Worker<-main output segment key. */
char input_shm_key[OAK_IPC_SHM_KEY_CAP]; /**< Main->worker input key (optional). */
int32_t input_slots; /**< Number of main->worker input frame slots. */
int32_t output_slots; /**< Number of worker->main output frame slots. */
int64_t slot_data_bytes; /**< Per-output-slot pixel block size. */
int64_t input_slot_data_bytes; /**< Per-input-slot pixel block size. */
} oak_ipc_handshake;
/**
* @brief Request a frame render: node uuid, time, video params.
*
* Field-for-field equivalent of the C++ RenderFrameMsg. `input_slots` holds
* `input_slot_count` entries; the legacy scalar `input_slot` (-1 = none) is
* kept in sync by the parse fallback exactly like the Qt implementation.
*/
typedef struct oak_ipc_render_frame {
int64_t ticket_id; /**< Correlates with the eventual frame_ready. */
char node_uuid[OAK_IPC_NODE_UUID_CAP]; /**< Viewer node stable uuid. */
int64_t time_num;
int64_t time_den;
int32_t width; /**< Forced output size (0 = use graph default). */
int32_t height;
int32_t format; /**< Forced PixelFormat::Format (-1 = default/INVALID). */
int32_t channel_count; /**< 0 = default. */
int32_t mode; /**< RenderMode::Mode. */
int32_t input_slot; /**< Optional decoded input slot (-1 = none). */
int32_t input_slots[OAK_IPC_INPUT_SLOTS_CAP]; /**< Ordered decoded input slots. */
int32_t input_slot_count; /**< Number of valid input_slots entries. */
/* Output color transform; ignored unless has_color_transform != 0. */
int32_t has_color_transform;
int32_t color_is_display;
char color_output[OAK_IPC_COLOR_STR_CAP];
char color_view[OAK_IPC_COLOR_STR_CAP];
char color_look[OAK_IPC_COLOR_STR_CAP];
} oak_ipc_render_frame;
/**
* @brief A rendered frame is published; carries the output slot + ticket.
*/
typedef struct oak_ipc_frame_ready {
int64_t ticket_id;
int32_t output_slot; /**< Index into the worker->main output FrameSlotPool. */
} oak_ipc_frame_ready;
/**
* @brief Abandon an in-flight ticket by id.
*/
typedef struct oak_ipc_cancel {
int64_t ticket_id;
} oak_ipc_cancel;
/**
* @brief Path to a temporary file holding the serialized node graph.
*/
typedef struct oak_ipc_load_graph {
char path[OAK_IPC_PATH_CAP];
} oak_ipc_load_graph;
/**
* @brief Identify the message type of one compact JSON line.
*
* Returns OAK_IPC_MSGTYPE_UNKNOWN for malformed JSON or an unrecognized
* "type" field.
*/
OAKENGINE_API oak_ipc_msgtype oakengine_ipc_message_type(const char *json);
/**
* @brief Serialize to compact JSON (buf/size convention). Returns -1 if
* `self` is NULL.
*/
OAKENGINE_API int oakengine_ipc_handshake_to_json(
const oak_ipc_handshake *self, char *buf, int buf_size);
/**
* @brief Parse compact JSON. Returns 1 on success, 0 on type mismatch or
* malformed input.
*/
OAKENGINE_API int oakengine_ipc_handshake_parse(const char *json,
oak_ipc_handshake *out);
OAKENGINE_API int oakengine_ipc_render_frame_to_json(
const oak_ipc_render_frame *self, char *buf, int buf_size);
OAKENGINE_API int oakengine_ipc_render_frame_parse(const char *json,
oak_ipc_render_frame *out);
OAKENGINE_API int oakengine_ipc_frame_ready_to_json(
const oak_ipc_frame_ready *self, char *buf, int buf_size);
OAKENGINE_API int oakengine_ipc_frame_ready_parse(const char *json,
oak_ipc_frame_ready *out);
OAKENGINE_API int oakengine_ipc_cancel_to_json(const oak_ipc_cancel *self,
char *buf, int buf_size);
OAKENGINE_API int oakengine_ipc_cancel_parse(const char *json,
oak_ipc_cancel *out);
OAKENGINE_API int oakengine_ipc_load_graph_to_json(
const oak_ipc_load_graph *self, char *buf, int buf_size);
OAKENGINE_API int oakengine_ipc_load_graph_parse(const char *json,
oak_ipc_load_graph *out);
/**
* @brief Build the payload-less shutdown message (buf/size convention).
*/
OAKENGINE_API int oakengine_ipc_shutdown_to_json(char *buf, int buf_size);
/**
* @brief Returns 1 if `json` is a shutdown message, 0 otherwise.
*/
OAKENGINE_API int oakengine_ipc_shutdown_parse(const char *json);
/**
* @brief Build a worker-side error report with a human-readable message
* (buf/size convention).
*/
OAKENGINE_API int oakengine_ipc_error_to_json(const char *message, char *buf,
int buf_size);
/**
* @brief Parse an error message; the text is written into message_buf
* (truncated at OAK_IPC_ERROR_MESSAGE_CAP-style buf size). Returns 1 on
* success, 0 otherwise.
*/
OAKENGINE_API int oakengine_ipc_error_parse(const char *json,
char *message_buf,
int message_buf_size);
#ifdef __cplusplus
}
#endif
#endif /* OAKENGINE_IPC_H */