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)
This commit is contained in:
@@ -0,0 +1,623 @@
|
||||
/***
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
#include "oakengine/ipc.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QtGlobal>
|
||||
|
||||
#include "oliveimpl/render/ipc/frameslotpool.h"
|
||||
#include "oliveimpl/render/ipc/ipcmessage.h"
|
||||
#include "oliveimpl/render/ipc/sharedmemoryregion.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
namespace internal_ipc = olive::engine::internal::ipc;
|
||||
|
||||
olive::engine::internal::ipc::SharedMemoryRegion *impl(OakSharedMemoryRegion *h)
|
||||
{
|
||||
return reinterpret_cast<olive::engine::internal::ipc::SharedMemoryRegion *>(
|
||||
h);
|
||||
}
|
||||
|
||||
const olive::engine::internal::ipc::SharedMemoryRegion *
|
||||
impl(const OakSharedMemoryRegion *h)
|
||||
{
|
||||
return reinterpret_cast<
|
||||
const olive::engine::internal::ipc::SharedMemoryRegion *>(h);
|
||||
}
|
||||
|
||||
OakSharedMemoryRegion *
|
||||
wrap(olive::engine::internal::ipc::SharedMemoryRegion *r)
|
||||
{
|
||||
return reinterpret_cast<OakSharedMemoryRegion *>(r);
|
||||
}
|
||||
|
||||
olive::engine::internal::ipc::FrameSlotPool *impl(OakFrameSlotPool *h)
|
||||
{
|
||||
return reinterpret_cast<olive::engine::internal::ipc::FrameSlotPool *>(h);
|
||||
}
|
||||
|
||||
const olive::engine::internal::ipc::FrameSlotPool *
|
||||
impl(const OakFrameSlotPool *h)
|
||||
{
|
||||
return reinterpret_cast<const olive::engine::internal::ipc::FrameSlotPool *>(
|
||||
h);
|
||||
}
|
||||
|
||||
OakFrameSlotPool *wrap(olive::engine::internal::ipc::FrameSlotPool *p)
|
||||
{
|
||||
return reinterpret_cast<OakFrameSlotPool *>(p);
|
||||
}
|
||||
|
||||
// Copy a QString into a fixed-capacity C buffer, always NUL-terminating and
|
||||
// truncating what does not fit.
|
||||
void copy_to_buf(const QString &s, char *dst, size_t cap)
|
||||
{
|
||||
const QByteArray utf = s.toUtf8();
|
||||
const size_t n = qMin(size_t(utf.size()), cap - 1);
|
||||
memcpy(dst, utf.constData(), n);
|
||||
dst[n] = '\0';
|
||||
}
|
||||
|
||||
// buf/size convention: returns the would-be length excluding the NUL.
|
||||
int string_to_buf(const QString &s, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray utf = s.toUtf8();
|
||||
if (buf && buf_size > 0) {
|
||||
snprintf(buf, size_t(buf_size), "%s", utf.constData());
|
||||
}
|
||||
return int(utf.size());
|
||||
}
|
||||
|
||||
int object_to_buf(const QJsonObject &o, char *buf, int buf_size)
|
||||
{
|
||||
const QByteArray json = QJsonDocument(o).toJson(QJsonDocument::Compact);
|
||||
if (buf && buf_size > 0) {
|
||||
snprintf(buf, size_t(buf_size), "%s", json.constData());
|
||||
}
|
||||
return int(json.size());
|
||||
}
|
||||
|
||||
bool parse_object(const char *json, QJsonObject *out)
|
||||
{
|
||||
if (!json) {
|
||||
return false;
|
||||
}
|
||||
QJsonParseError err;
|
||||
const QJsonDocument doc = QJsonDocument::fromJson(json, &err);
|
||||
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
|
||||
return false;
|
||||
}
|
||||
*out = doc.object();
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- POD <-> impl message conversions ------------------------------------
|
||||
|
||||
void to_c(const internal_ipc::HandshakeMsg &in, oak_ipc_handshake *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->protocol_version = in.protocol_version;
|
||||
copy_to_buf(in.shm_key, out->shm_key, sizeof(out->shm_key));
|
||||
copy_to_buf(in.input_shm_key, out->input_shm_key,
|
||||
sizeof(out->input_shm_key));
|
||||
out->input_slots = in.input_slots;
|
||||
out->output_slots = in.output_slots;
|
||||
out->slot_data_bytes = in.slot_data_bytes;
|
||||
out->input_slot_data_bytes = in.input_slot_data_bytes;
|
||||
}
|
||||
|
||||
void from_c(const oak_ipc_handshake *in, internal_ipc::HandshakeMsg *out)
|
||||
{
|
||||
out->protocol_version = in->protocol_version;
|
||||
out->shm_key = QString::fromUtf8(in->shm_key);
|
||||
out->input_shm_key = QString::fromUtf8(in->input_shm_key);
|
||||
out->input_slots = in->input_slots;
|
||||
out->output_slots = in->output_slots;
|
||||
out->slot_data_bytes = in->slot_data_bytes;
|
||||
out->input_slot_data_bytes = in->input_slot_data_bytes;
|
||||
}
|
||||
|
||||
void to_c(const internal_ipc::RenderFrameMsg &in, oak_ipc_render_frame *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->ticket_id = in.ticket_id;
|
||||
copy_to_buf(in.node_uuid, out->node_uuid, sizeof(out->node_uuid));
|
||||
out->time_num = in.time_num;
|
||||
out->time_den = in.time_den;
|
||||
out->width = in.width;
|
||||
out->height = in.height;
|
||||
out->format = in.format;
|
||||
out->channel_count = in.channel_count;
|
||||
out->mode = in.mode;
|
||||
out->input_slot = in.input_slot;
|
||||
const int count = qMin(int(in.input_slots.size()), OAK_IPC_INPUT_SLOTS_CAP);
|
||||
out->input_slot_count = count;
|
||||
for (int i = 0; i < count; i++) {
|
||||
out->input_slots[i] = in.input_slots.at(i);
|
||||
}
|
||||
out->has_color_transform = in.has_color_transform ? 1 : 0;
|
||||
out->color_is_display = in.color_is_display ? 1 : 0;
|
||||
copy_to_buf(in.color_output, out->color_output,
|
||||
sizeof(out->color_output));
|
||||
copy_to_buf(in.color_view, out->color_view, sizeof(out->color_view));
|
||||
copy_to_buf(in.color_look, out->color_look, sizeof(out->color_look));
|
||||
}
|
||||
|
||||
void from_c(const oak_ipc_render_frame *in, internal_ipc::RenderFrameMsg *out)
|
||||
{
|
||||
out->ticket_id = in->ticket_id;
|
||||
out->node_uuid = QString::fromUtf8(in->node_uuid);
|
||||
out->time_num = in->time_num;
|
||||
out->time_den = in->time_den;
|
||||
out->width = in->width;
|
||||
out->height = in->height;
|
||||
out->format = in->format;
|
||||
out->channel_count = in->channel_count;
|
||||
out->mode = in->mode;
|
||||
out->input_slot = in->input_slot;
|
||||
out->input_slots.clear();
|
||||
const int count = qMin(in->input_slot_count, OAK_IPC_INPUT_SLOTS_CAP);
|
||||
for (int i = 0; i < count; i++) {
|
||||
out->input_slots.append(in->input_slots[i]);
|
||||
}
|
||||
out->has_color_transform = in->has_color_transform != 0;
|
||||
out->color_is_display = in->color_is_display != 0;
|
||||
out->color_output = QString::fromUtf8(in->color_output);
|
||||
out->color_view = QString::fromUtf8(in->color_view);
|
||||
out->color_look = QString::fromUtf8(in->color_look);
|
||||
}
|
||||
|
||||
void to_c(const internal_ipc::FrameReadyMsg &in, oak_ipc_frame_ready *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->ticket_id = in.ticket_id;
|
||||
out->output_slot = in.output_slot;
|
||||
}
|
||||
|
||||
void from_c(const oak_ipc_frame_ready *in, internal_ipc::FrameReadyMsg *out)
|
||||
{
|
||||
out->ticket_id = in->ticket_id;
|
||||
out->output_slot = in->output_slot;
|
||||
}
|
||||
|
||||
void to_c(const internal_ipc::CancelMsg &in, oak_ipc_cancel *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->ticket_id = in.ticket_id;
|
||||
}
|
||||
|
||||
void from_c(const oak_ipc_cancel *in, internal_ipc::CancelMsg *out)
|
||||
{
|
||||
out->ticket_id = in->ticket_id;
|
||||
}
|
||||
|
||||
void to_c(const internal_ipc::LoadGraphMsg &in, oak_ipc_load_graph *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
copy_to_buf(in.path, out->path, sizeof(out->path));
|
||||
}
|
||||
|
||||
void from_c(const oak_ipc_load_graph *in, internal_ipc::LoadGraphMsg *out)
|
||||
{
|
||||
out->path = QString::fromUtf8(in->path);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
extern "C"
|
||||
{
|
||||
|
||||
/* ---- SharedMemoryRegion ------------------------------------------------- */
|
||||
|
||||
OakSharedMemoryRegion *oakengine_ipc_shm_create(void)
|
||||
{
|
||||
return wrap(new internal_ipc::SharedMemoryRegion());
|
||||
}
|
||||
|
||||
void oakengine_ipc_shm_free(OakSharedMemoryRegion *self)
|
||||
{
|
||||
delete impl(self);
|
||||
}
|
||||
|
||||
int oakengine_ipc_shm_open(OakSharedMemoryRegion *self, const char *key,
|
||||
size_t size, oak_ipc_shm_mode mode)
|
||||
{
|
||||
if (!self || !key) {
|
||||
return 0;
|
||||
}
|
||||
const internal_ipc::SharedMemoryRegion::Mode m =
|
||||
mode == OAK_IPC_SHM_MODE_CREATE ?
|
||||
internal_ipc::SharedMemoryRegion::k_create :
|
||||
internal_ipc::SharedMemoryRegion::k_attach;
|
||||
return impl(self)->open(QString::fromUtf8(key), size, m) ? 1 : 0;
|
||||
}
|
||||
|
||||
void oakengine_ipc_shm_close(OakSharedMemoryRegion *self)
|
||||
{
|
||||
if (self) {
|
||||
impl(self)->close();
|
||||
}
|
||||
}
|
||||
|
||||
int oakengine_ipc_shm_is_valid(const OakSharedMemoryRegion *self)
|
||||
{
|
||||
return self && impl(self)->is_valid() ? 1 : 0;
|
||||
}
|
||||
|
||||
void *oakengine_ipc_shm_data(OakSharedMemoryRegion *self)
|
||||
{
|
||||
return self ? impl(self)->data() : nullptr;
|
||||
}
|
||||
|
||||
size_t oakengine_ipc_shm_size(const OakSharedMemoryRegion *self)
|
||||
{
|
||||
return self ? impl(self)->size() : 0;
|
||||
}
|
||||
|
||||
int oakengine_ipc_shm_key(const OakSharedMemoryRegion *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
return string_to_buf(self ? impl(self)->key() : QString(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_ipc_shm_error(const OakSharedMemoryRegion *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
return string_to_buf(self ? impl(self)->error() : QString(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_ipc_shm_make_key(int64_t owner_pid, int worker_index, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
return string_to_buf(
|
||||
internal_ipc::SharedMemoryRegion::make_key(owner_pid, worker_index),
|
||||
buf, buf_size);
|
||||
}
|
||||
|
||||
/* ---- FrameSlotPool ------------------------------------------------------ */
|
||||
|
||||
size_t oakengine_ipc_framepool_bytes_needed(uint32_t slot_count,
|
||||
size_t slot_data_bytes)
|
||||
{
|
||||
return internal_ipc::FrameSlotPool::bytes_needed(slot_count,
|
||||
slot_data_bytes);
|
||||
}
|
||||
|
||||
OakFrameSlotPool *oakengine_ipc_framepool_create(void *mem,
|
||||
uint32_t slot_count,
|
||||
size_t slot_data_bytes)
|
||||
{
|
||||
if (!mem) {
|
||||
return nullptr;
|
||||
}
|
||||
return wrap(new internal_ipc::FrameSlotPool(
|
||||
internal_ipc::FrameSlotPool::create(mem, slot_count,
|
||||
slot_data_bytes)));
|
||||
}
|
||||
|
||||
OakFrameSlotPool *oakengine_ipc_framepool_attach(void *mem)
|
||||
{
|
||||
if (!mem) {
|
||||
return nullptr;
|
||||
}
|
||||
return wrap(new internal_ipc::FrameSlotPool(
|
||||
internal_ipc::FrameSlotPool::attach(mem)));
|
||||
}
|
||||
|
||||
OakFrameSlotPool *oakengine_ipc_framepool_copy(const OakFrameSlotPool *self)
|
||||
{
|
||||
if (!self) {
|
||||
return nullptr;
|
||||
}
|
||||
return wrap(new internal_ipc::FrameSlotPool(*impl(self)));
|
||||
}
|
||||
|
||||
void oakengine_ipc_framepool_free(OakFrameSlotPool *self)
|
||||
{
|
||||
delete impl(self);
|
||||
}
|
||||
|
||||
int oakengine_ipc_framepool_is_valid(const OakFrameSlotPool *self)
|
||||
{
|
||||
return self && impl(self)->is_valid() ? 1 : 0;
|
||||
}
|
||||
|
||||
uint32_t oakengine_ipc_framepool_slot_count(const OakFrameSlotPool *self)
|
||||
{
|
||||
return self ? impl(self)->slot_count() : 0;
|
||||
}
|
||||
|
||||
size_t oakengine_ipc_framepool_slot_data_bytes(const OakFrameSlotPool *self)
|
||||
{
|
||||
return self ? impl(self)->slot_data_bytes() : 0;
|
||||
}
|
||||
|
||||
int oakengine_ipc_framepool_acquire(OakFrameSlotPool *self, uint32_t *index)
|
||||
{
|
||||
return self && index && impl(self)->is_valid() &&
|
||||
impl(self)->acquire(index) ?
|
||||
1 :
|
||||
0;
|
||||
}
|
||||
|
||||
void *oakengine_ipc_framepool_slot_data(OakFrameSlotPool *self, uint32_t index)
|
||||
{
|
||||
return self && impl(self)->is_valid() ? impl(self)->slot_data(index) :
|
||||
nullptr;
|
||||
}
|
||||
|
||||
const void *oakengine_ipc_framepool_slot_data_const(
|
||||
const OakFrameSlotPool *self, uint32_t index)
|
||||
{
|
||||
return self && impl(self)->is_valid() ? impl(self)->slot_data(index) :
|
||||
nullptr;
|
||||
}
|
||||
|
||||
oak_frame_slot_meta *oakengine_ipc_framepool_meta(OakFrameSlotPool *self,
|
||||
uint32_t index)
|
||||
{
|
||||
return self && impl(self)->is_valid() ? impl(self)->meta(index) : nullptr;
|
||||
}
|
||||
|
||||
const oak_frame_slot_meta *oakengine_ipc_framepool_meta_const(
|
||||
const OakFrameSlotPool *self, uint32_t index)
|
||||
{
|
||||
return self && impl(self)->is_valid() ? impl(self)->meta(index) : nullptr;
|
||||
}
|
||||
|
||||
int oakengine_ipc_framepool_publish(OakFrameSlotPool *self, uint32_t index)
|
||||
{
|
||||
return self && impl(self)->is_valid() && impl(self)->publish(index) ? 1 : 0;
|
||||
}
|
||||
|
||||
int oakengine_ipc_framepool_consume(OakFrameSlotPool *self, uint32_t *index)
|
||||
{
|
||||
return self && index && impl(self)->is_valid() &&
|
||||
impl(self)->consume(index) ?
|
||||
1 :
|
||||
0;
|
||||
}
|
||||
|
||||
int oakengine_ipc_framepool_release(OakFrameSlotPool *self, uint32_t index)
|
||||
{
|
||||
return self && impl(self)->is_valid() && impl(self)->release(index) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ---- Control-plane messages --------------------------------------------- */
|
||||
|
||||
oak_ipc_msgtype oakengine_ipc_message_type(const char *json)
|
||||
{
|
||||
QJsonObject o;
|
||||
if (!parse_object(json, &o)) {
|
||||
return OAK_IPC_MSGTYPE_UNKNOWN;
|
||||
}
|
||||
const QString type = o[QStringLiteral("type")].toString();
|
||||
if (type == QLatin1String(internal_ipc::msgtype::k_handshake)) {
|
||||
return OAK_IPC_MSGTYPE_HANDSHAKE;
|
||||
}
|
||||
if (type == QLatin1String(internal_ipc::msgtype::k_load_graph)) {
|
||||
return OAK_IPC_MSGTYPE_LOAD_GRAPH;
|
||||
}
|
||||
if (type == QLatin1String(internal_ipc::msgtype::k_render_frame)) {
|
||||
return OAK_IPC_MSGTYPE_RENDER_FRAME;
|
||||
}
|
||||
if (type == QLatin1String(internal_ipc::msgtype::k_frame_ready)) {
|
||||
return OAK_IPC_MSGTYPE_FRAME_READY;
|
||||
}
|
||||
if (type == QLatin1String(internal_ipc::msgtype::k_cancel)) {
|
||||
return OAK_IPC_MSGTYPE_CANCEL;
|
||||
}
|
||||
if (type == QLatin1String(internal_ipc::msgtype::k_graph_update)) {
|
||||
return OAK_IPC_MSGTYPE_GRAPH_UPDATE;
|
||||
}
|
||||
if (type == QLatin1String(internal_ipc::msgtype::k_shutdown)) {
|
||||
return OAK_IPC_MSGTYPE_SHUTDOWN;
|
||||
}
|
||||
if (type == QLatin1String(internal_ipc::msgtype::k_error)) {
|
||||
return OAK_IPC_MSGTYPE_ERROR;
|
||||
}
|
||||
return OAK_IPC_MSGTYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
int oakengine_ipc_handshake_to_json(const oak_ipc_handshake *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return -1;
|
||||
}
|
||||
internal_ipc::HandshakeMsg in;
|
||||
from_c(self, &in);
|
||||
return object_to_buf(in.to_json(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_ipc_handshake_parse(const char *json, oak_ipc_handshake *out)
|
||||
{
|
||||
if (!out) {
|
||||
return 0;
|
||||
}
|
||||
QJsonObject o;
|
||||
internal_ipc::HandshakeMsg in;
|
||||
if (!parse_object(json, &o) ||
|
||||
!internal_ipc::HandshakeMsg::from_json(o, &in)) {
|
||||
return 0;
|
||||
}
|
||||
to_c(in, out);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int oakengine_ipc_render_frame_to_json(const oak_ipc_render_frame *self,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return -1;
|
||||
}
|
||||
internal_ipc::RenderFrameMsg in;
|
||||
from_c(self, &in);
|
||||
return object_to_buf(in.to_json(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_ipc_render_frame_parse(const char *json,
|
||||
oak_ipc_render_frame *out)
|
||||
{
|
||||
if (!out) {
|
||||
return 0;
|
||||
}
|
||||
QJsonObject o;
|
||||
internal_ipc::RenderFrameMsg in;
|
||||
if (!parse_object(json, &o) ||
|
||||
!internal_ipc::RenderFrameMsg::from_json(o, &in)) {
|
||||
return 0;
|
||||
}
|
||||
to_c(in, out);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int oakengine_ipc_frame_ready_to_json(const oak_ipc_frame_ready *self,
|
||||
char *buf, int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return -1;
|
||||
}
|
||||
internal_ipc::FrameReadyMsg in;
|
||||
from_c(self, &in);
|
||||
return object_to_buf(in.to_json(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_ipc_frame_ready_parse(const char *json,
|
||||
oak_ipc_frame_ready *out)
|
||||
{
|
||||
if (!out) {
|
||||
return 0;
|
||||
}
|
||||
QJsonObject o;
|
||||
internal_ipc::FrameReadyMsg in;
|
||||
if (!parse_object(json, &o) ||
|
||||
!internal_ipc::FrameReadyMsg::from_json(o, &in)) {
|
||||
return 0;
|
||||
}
|
||||
to_c(in, out);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int oakengine_ipc_cancel_to_json(const oak_ipc_cancel *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return -1;
|
||||
}
|
||||
internal_ipc::CancelMsg in;
|
||||
from_c(self, &in);
|
||||
return object_to_buf(in.to_json(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_ipc_cancel_parse(const char *json, oak_ipc_cancel *out)
|
||||
{
|
||||
if (!out) {
|
||||
return 0;
|
||||
}
|
||||
QJsonObject o;
|
||||
internal_ipc::CancelMsg in;
|
||||
if (!parse_object(json, &o) ||
|
||||
!internal_ipc::CancelMsg::from_json(o, &in)) {
|
||||
return 0;
|
||||
}
|
||||
to_c(in, out);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int oakengine_ipc_load_graph_to_json(const oak_ipc_load_graph *self, char *buf,
|
||||
int buf_size)
|
||||
{
|
||||
if (!self) {
|
||||
return -1;
|
||||
}
|
||||
internal_ipc::LoadGraphMsg in;
|
||||
from_c(self, &in);
|
||||
return object_to_buf(in.to_json(), buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_ipc_load_graph_parse(const char *json, oak_ipc_load_graph *out)
|
||||
{
|
||||
if (!out) {
|
||||
return 0;
|
||||
}
|
||||
QJsonObject o;
|
||||
internal_ipc::LoadGraphMsg in;
|
||||
if (!parse_object(json, &o) ||
|
||||
!internal_ipc::LoadGraphMsg::from_json(o, &in)) {
|
||||
return 0;
|
||||
}
|
||||
to_c(in, out);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int oakengine_ipc_shutdown_to_json(char *buf, int buf_size)
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("type")] = internal_ipc::msgtype::k_shutdown;
|
||||
return object_to_buf(o, buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_ipc_shutdown_parse(const char *json)
|
||||
{
|
||||
QJsonObject o;
|
||||
if (!parse_object(json, &o)) {
|
||||
return 0;
|
||||
}
|
||||
return o[QStringLiteral("type")].toString() ==
|
||||
QLatin1String(internal_ipc::msgtype::k_shutdown) ?
|
||||
1 :
|
||||
0;
|
||||
}
|
||||
|
||||
int oakengine_ipc_error_to_json(const char *message, char *buf, int buf_size)
|
||||
{
|
||||
QJsonObject o;
|
||||
o[QStringLiteral("type")] = internal_ipc::msgtype::k_error;
|
||||
o[QStringLiteral("message")] = QString::fromUtf8(message ? message : "");
|
||||
return object_to_buf(o, buf, buf_size);
|
||||
}
|
||||
|
||||
int oakengine_ipc_error_parse(const char *json, char *message_buf,
|
||||
int message_buf_size)
|
||||
{
|
||||
QJsonObject o;
|
||||
if (!parse_object(json, &o) ||
|
||||
o[QStringLiteral("type")].toString() !=
|
||||
QLatin1String(internal_ipc::msgtype::k_error)) {
|
||||
return 0;
|
||||
}
|
||||
if (message_buf && message_buf_size > 0) {
|
||||
copy_to_buf(o[QStringLiteral("message")].toString(), message_buf,
|
||||
size_t(message_buf_size));
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // extern "C"
|
||||
@@ -0,0 +1,182 @@
|
||||
/***
|
||||
|
||||
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 OAK_OLIVEIMPL_RENDER_IPC_FRAMESLOTPOOL_H
|
||||
#define OAK_OLIVEIMPL_RENDER_IPC_FRAMESLOTPOOL_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "oakengine/ipc.h"
|
||||
#include "oakengine/spscringbuffer.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace engine
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Per-slot metadata describing the frame currently occupying a slot.
|
||||
*
|
||||
* Trivially-copyable POD that lives in shared memory alongside the pixel data. Carries everything
|
||||
* the consumer needs to reconstruct an olive::Frame without any out-of-band information. We store
|
||||
* the Rational timestamp as an explicit numerator/denominator pair to stay POD (olive::Rational is
|
||||
* not guaranteed shared-memory-safe).
|
||||
*
|
||||
* This is the C ABI oak_frame_slot_meta struct, aliased so the version-1 wire layout the app and
|
||||
* the render worker agree on is defined exactly once, in oakengine/ipc.h.
|
||||
*/
|
||||
typedef oak_frame_slot_meta FrameSlotMeta;
|
||||
|
||||
/**
|
||||
* @brief A fixed-size pool of equal-sized frame slots in shared memory, with lock-free hand-off.
|
||||
*
|
||||
* One pool models a single direction of frame flow (e.g. worker -> main for rendered output, or
|
||||
* main -> worker for decoded input). Ownership of a slot is transferred via two SPSC ring buffers
|
||||
* of slot indices, so no mutex is ever taken:
|
||||
*
|
||||
* - free_ring: indices of slots available to the FILLER. The drainer returns slots here.
|
||||
* - ready_ring: indices of slots holding a published frame, produced by the FILLER for the
|
||||
* DRAINER to consume.
|
||||
*
|
||||
* Lifecycle (filler = producer of frames, drainer = consumer of frames):
|
||||
* filler: Acquire() -> pop a free index -> write meta + pixels -> Publish() -> push to ready
|
||||
* drainer: Consume() -> pop a ready index -> read meta + pixels -> Release() -> push to free
|
||||
*
|
||||
* Because each ring has exactly one producer and one consumer (the filler owns free.Pop +
|
||||
* ready.Push, the drainer owns ready.Pop + free.Push), the SPSC invariant holds and the whole
|
||||
* exchange is lock-free.
|
||||
*
|
||||
* All slots are sized to `slot_data_bytes`, computed for the maximum supported frame (e.g. 8K RGBA
|
||||
* half-float). Frames smaller than that simply use a prefix of the slot.
|
||||
*
|
||||
* The pool does NOT own the memory; it is constructed over a SharedMemoryRegion mapping. Use
|
||||
* BytesNeeded() to size that region.
|
||||
*/
|
||||
class FrameSlotPool {
|
||||
public:
|
||||
/**
|
||||
* @brief Total bytes a region must provide to back a pool of `slot_count` x `slot_data_bytes`.
|
||||
*/
|
||||
static size_t 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).
|
||||
*
|
||||
* Initializes both rings, seeds the free ring with every slot index, and zeroes metadata.
|
||||
* `mem` must provide at least BytesNeeded(slot_count, slot_data_bytes) bytes.
|
||||
*/
|
||||
static FrameSlotPool create(void *mem, uint32_t slot_count,
|
||||
size_t slot_data_bytes);
|
||||
|
||||
/**
|
||||
* @brief Map an existing, already-initialized pool (peer side).
|
||||
*
|
||||
* Reads slot_count/slot_data_bytes from the in-memory header written by Create().
|
||||
*/
|
||||
static FrameSlotPool attach(void *mem);
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return header_ != nullptr;
|
||||
}
|
||||
|
||||
uint32_t slot_count() const;
|
||||
size_t slot_data_bytes() const;
|
||||
|
||||
// ---- Filler side ----
|
||||
|
||||
/**
|
||||
* @brief Take ownership of a free slot. Returns false (and leaves *index untouched) if none free.
|
||||
*/
|
||||
bool acquire(uint32_t *index);
|
||||
|
||||
/**
|
||||
* @brief Pointer to a slot's pixel data block (slot_data_bytes available).
|
||||
*/
|
||||
void *slot_data(uint32_t index);
|
||||
|
||||
/**
|
||||
* @brief Mutable metadata for a slot. Filler writes this before Publish().
|
||||
*/
|
||||
FrameSlotMeta *meta(uint32_t index);
|
||||
|
||||
/**
|
||||
* @brief Publish a filled slot to the drainer. Must follow a successful Acquire() of `index`.
|
||||
*/
|
||||
bool publish(uint32_t index);
|
||||
|
||||
// ---- Drainer side ----
|
||||
|
||||
/**
|
||||
* @brief Take the next published slot. Returns false if nothing is ready.
|
||||
*/
|
||||
bool consume(uint32_t *index);
|
||||
|
||||
/**
|
||||
* @brief Return a consumed slot to the free pool for reuse. Must follow Consume() of `index`.
|
||||
*/
|
||||
bool release(uint32_t index);
|
||||
|
||||
const FrameSlotMeta *meta(uint32_t index) const;
|
||||
const void *slot_data(uint32_t index) const;
|
||||
|
||||
public:
|
||||
FrameSlotPool() = default;
|
||||
|
||||
private:
|
||||
struct Header {
|
||||
uint32_t magic;
|
||||
uint32_t slot_count;
|
||||
uint64_t slot_data_bytes;
|
||||
// Byte offsets from the start of the segment to each sub-region.
|
||||
uint64_t free_ring_offset;
|
||||
uint64_t ready_ring_offset;
|
||||
uint64_t meta_offset;
|
||||
uint64_t data_offset;
|
||||
};
|
||||
|
||||
static constexpr uint32_t k_magic = 0x4F4B5350; // 'OKSP'
|
||||
|
||||
// Ring capacity must exceed slot_count by one because a ring can hold at most capacity-1 entries
|
||||
// and we need to be able to enqueue every slot at once.
|
||||
static uint32_t ring_capacity(uint32_t slot_count)
|
||||
{
|
||||
return slot_count + 1;
|
||||
}
|
||||
|
||||
uint8_t *base_ = nullptr;
|
||||
Header *header_ = nullptr;
|
||||
olive::ipc::SpscRingBuffer *free_ring_ = nullptr;
|
||||
olive::ipc::SpscRingBuffer *ready_ring_ = nullptr;
|
||||
FrameSlotMeta *meta_ = nullptr;
|
||||
uint8_t *data_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace internal
|
||||
} // namespace engine
|
||||
} // namespace olive
|
||||
|
||||
#endif // OAK_OLIVEIMPL_RENDER_IPC_FRAMESLOTPOOL_H
|
||||
@@ -0,0 +1,167 @@
|
||||
/***
|
||||
|
||||
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 OAK_OLIVEIMPL_RENDER_IPC_IPCMESSAGE_H
|
||||
#define OAK_OLIVEIMPL_RENDER_IPC_IPCMESSAGE_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <QByteArray>
|
||||
#include <QJsonObject>
|
||||
#include <QString>
|
||||
#include <QVector>
|
||||
|
||||
class QIODevice;
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace engine
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief Control-plane protocol exchanged over stdio between main and render worker.
|
||||
*
|
||||
* The wire format is NDJSON: one compact QJsonObject per line, terminated by '\n'. This is
|
||||
* deliberately human-readable so the channel can be inspected live with `tee`/`cat` and test
|
||||
* messages can be injected by hand. The stdio channel carries only low-frequency control traffic;
|
||||
* bulk pixel data travels through the shared-memory FrameSlotPool, and the (potentially large)
|
||||
* serialized node graph travels via a temporary file referenced by path.
|
||||
*
|
||||
* Every message object has a "type" string field. Directionality (M = main, W = worker):
|
||||
* "handshake" M<->W Negotiate protocol version and announce shared-memory key/geometry.
|
||||
* "load_graph" M ->W Path to a temporary file holding the serialized node graph.
|
||||
* "render_frame" M ->W Request a frame: node uuid, time, video params.
|
||||
* "frame_ready" W ->M A rendered frame is published; carries the output-slot index + ticket.
|
||||
* "cancel" M ->W Abandon an in-flight ticket by id.
|
||||
* "graph_update" M ->W (Reserved, Phase 6) Incremental graph mutation, mirrors ProjectCopier.
|
||||
* "shutdown" M ->W Finish current work and exit cleanly.
|
||||
* "error" W ->M Worker-side failure report (human-readable "message" field).
|
||||
*/
|
||||
namespace msgtype
|
||||
{
|
||||
constexpr const char *k_handshake = "handshake";
|
||||
constexpr const char *k_load_graph = "load_graph";
|
||||
constexpr const char *k_render_frame = "render_frame";
|
||||
constexpr const char *k_frame_ready = "frame_ready";
|
||||
constexpr const char *k_cancel = "cancel";
|
||||
constexpr const char *k_graph_update = "graph_update";
|
||||
constexpr const char *k_shutdown = "shutdown";
|
||||
constexpr const char *k_error = "error";
|
||||
} // namespace msgtype
|
||||
|
||||
/**
|
||||
* @brief Write one NDJSON message line to `device`.
|
||||
*
|
||||
* Serializes `obj` to compact JSON, appends '\n', and writes the whole line in one call. Returns
|
||||
* true only if the full line was written.
|
||||
*/
|
||||
bool write_message(QIODevice *device, const QJsonObject &obj);
|
||||
|
||||
/**
|
||||
* @brief Pull one complete NDJSON line out of `buffer` and parse it.
|
||||
*
|
||||
* If `buffer` contains at least one '\n', the leading line is removed, parsed as JSON, and returned
|
||||
* via `out` (true). If no complete line is buffered yet, leaves `buffer` untouched and returns
|
||||
* false. Malformed lines are skipped (removed) and reported via `*ok = false` so the reader can log
|
||||
* and continue rather than wedge. Supports the typical "append bytes as they arrive, then drain
|
||||
* complete lines" reader loop on a pipe.
|
||||
*/
|
||||
bool read_message(QByteArray *buffer, QJsonObject *out, bool *ok = nullptr);
|
||||
|
||||
// ---- Typed message builders / parsers -------------------------------------------------------
|
||||
//
|
||||
// Thin helpers that construct or read the QJsonObject for each message type, keeping field names in
|
||||
// one place so main and worker agree. Fields use plain JSON numbers/strings; 64-bit ids are stored
|
||||
// as JSON numbers (doubles exactly represent integers up to 2^53, ample for our counters).
|
||||
|
||||
struct HandshakeMsg {
|
||||
int protocol_version = 0;
|
||||
QString shm_key; ///< Worker->main output shared-memory segment key.
|
||||
QString
|
||||
input_shm_key; ///< Main->worker input shared-memory segment key (optional).
|
||||
int input_slots = 0; ///< Number of main->worker input frame slots.
|
||||
int output_slots = 0; ///< Number of worker->main output frame slots.
|
||||
qint64 slot_data_bytes = 0; ///< Per-output-slot pixel block size.
|
||||
qint64 input_slot_data_bytes = 0; ///< Per-input-slot pixel block size.
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, HandshakeMsg *out);
|
||||
};
|
||||
|
||||
struct RenderFrameMsg {
|
||||
qint64 ticket_id =
|
||||
0; ///< Correlates this request with the eventual frame_ready.
|
||||
QString
|
||||
node_uuid; ///< Output/viewer node to render, by stable uuid in the loaded graph.
|
||||
qint64 time_num = 0;
|
||||
qint64 time_den = 1;
|
||||
int width = 0; ///< Forced output size (0 = use graph default).
|
||||
int height = 0;
|
||||
int format = -1; ///< Forced PixelFormat::Format (-1 = default/INVALID).
|
||||
int channel_count = 0; ///< 0 = default.
|
||||
int mode = 0; ///< RenderMode::Mode.
|
||||
int input_slot =
|
||||
-1; ///< Optional main->worker decoded input slot for footage nodes.
|
||||
QVector<int>
|
||||
input_slots; ///< Optional ordered decoded input slots for footage nodes.
|
||||
|
||||
// Output color transform to apply before returning the frame. When empty,
|
||||
// the worker returns the image in the project's reference space.
|
||||
bool has_color_transform = false;
|
||||
bool color_is_display = false;
|
||||
QString color_output;
|
||||
QString color_view;
|
||||
QString color_look;
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, RenderFrameMsg *out);
|
||||
};
|
||||
|
||||
struct FrameReadyMsg {
|
||||
qint64 ticket_id = 0;
|
||||
int output_slot = 0; ///< Index into the worker->main output FrameSlotPool.
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, FrameReadyMsg *out);
|
||||
};
|
||||
|
||||
struct CancelMsg {
|
||||
qint64 ticket_id = 0;
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, CancelMsg *out);
|
||||
};
|
||||
|
||||
struct LoadGraphMsg {
|
||||
QString path; ///< Temporary file holding the serialized node graph.
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, LoadGraphMsg *out);
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace internal
|
||||
} // namespace engine
|
||||
} // namespace olive
|
||||
|
||||
#endif // OAK_OLIVEIMPL_RENDER_IPC_IPCMESSAGE_H
|
||||
@@ -0,0 +1,129 @@
|
||||
/***
|
||||
|
||||
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 OAK_OLIVEIMPL_RENDER_IPC_SHAREDMEMORYREGION_H
|
||||
#define OAK_OLIVEIMPL_RENDER_IPC_SHAREDMEMORYREGION_H
|
||||
|
||||
#include <cstddef>
|
||||
#include <QString>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace engine
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
/**
|
||||
* @brief A named, fixed-size shared memory segment mapped into the process address space.
|
||||
*
|
||||
* One process Create()s the segment (owner); the peer process Attach()es to it by the same key.
|
||||
* The mapping is a raw contiguous byte range accessible via data() — the IPC 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.
|
||||
*
|
||||
* We deliberately use the raw OS primitives (POSIX shm_open + mmap, Windows CreateFileMapping +
|
||||
* MapViewOfFile) rather than QSharedMemory: QSharedMemory carries an implicit semaphore and a 1-byte
|
||||
* header convention, attaches/detaches with reference counting we don't want, and historically has
|
||||
* cross-platform lifetime quirks. For a render pipeline pushing large frames we want a plain mmap.
|
||||
*/
|
||||
class SharedMemoryRegion {
|
||||
public:
|
||||
enum Mode {
|
||||
/// Create (and own) the segment. Fails if it already exists; unlinks on destruction.
|
||||
k_create,
|
||||
/// Attach to a segment created by the peer. Does not unlink on destruction.
|
||||
k_attach
|
||||
};
|
||||
|
||||
SharedMemoryRegion();
|
||||
~SharedMemoryRegion();
|
||||
|
||||
SharedMemoryRegion(const SharedMemoryRegion &) = delete;
|
||||
SharedMemoryRegion &operator=(const SharedMemoryRegion &) = delete;
|
||||
|
||||
/**
|
||||
* @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 true on success. On failure, error() carries a human-readable reason.
|
||||
*/
|
||||
bool open(const QString &key, size_t size, Mode mode);
|
||||
|
||||
/**
|
||||
* @brief Unmap and (if owner) unlink the segment. Called automatically by the destructor.
|
||||
*/
|
||||
void close();
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return data_ != nullptr;
|
||||
}
|
||||
|
||||
void *data() const
|
||||
{
|
||||
return data_;
|
||||
}
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return size_;
|
||||
}
|
||||
|
||||
const QString &key() const
|
||||
{
|
||||
return key_;
|
||||
}
|
||||
|
||||
const QString &error() const
|
||||
{
|
||||
return error_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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.
|
||||
*/
|
||||
static QString make_key(qint64 owner_pid, int worker_index);
|
||||
|
||||
private:
|
||||
QString key_;
|
||||
size_t size_;
|
||||
void *data_;
|
||||
Mode mode_;
|
||||
QString error_;
|
||||
|
||||
#if defined(Q_OS_WIN)
|
||||
void *handle_; // HANDLE from CreateFileMapping/OpenFileMapping
|
||||
#else
|
||||
int fd_; // file descriptor from shm_open
|
||||
QString shm_name_; // the platform-prefixed name actually passed to shm_open
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace internal
|
||||
} // namespace engine
|
||||
} // namespace olive
|
||||
|
||||
#endif // OAK_OLIVEIMPL_RENDER_IPC_SHAREDMEMORYREGION_H
|
||||
Reference in New Issue
Block a user