Files
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

377 lines
12 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 OAK_IPC_IPCMESSAGE_H
#define OAK_IPC_IPCMESSAGE_H
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <QByteArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QString>
#include <QVector>
#include "oakengine/ipc.h"
class QIODevice;
namespace olive
{
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.
*
* Consumer-side wrapper over the liboakengine C ABI: the typed builders/parsers below convert
* through the oakengine_ipc_*_to_json/parse functions, and the QIODevice framing stays inline
* here. The public API is unchanged from the original implementation.
*
* 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 = OAKENGINE_IPC_MSGTYPE_HANDSHAKE;
constexpr const char *k_load_graph = OAKENGINE_IPC_MSGTYPE_LOAD_GRAPH;
constexpr const char *k_render_frame = OAKENGINE_IPC_MSGTYPE_RENDER_FRAME;
constexpr const char *k_frame_ready = OAKENGINE_IPC_MSGTYPE_FRAME_READY;
constexpr const char *k_cancel = OAKENGINE_IPC_MSGTYPE_CANCEL;
constexpr const char *k_graph_update = OAKENGINE_IPC_MSGTYPE_GRAPH_UPDATE;
constexpr const char *k_shutdown = OAKENGINE_IPC_MSGTYPE_SHUTDOWN;
constexpr const char *k_error = OAKENGINE_IPC_MSGTYPE_ERROR;
} // namespace msgtype
namespace detail
{
inline void copy_str(const QString &s, char *dst, size_t cap)
{
const QByteArray utf = s.toUtf8();
const size_t n = std::min(size_t(utf.size()), cap - 1);
memcpy(dst, utf.constData(), n);
dst[n] = '\0';
}
/**
* @brief Run a C to_json function (buf/size convention) and reparse the compact JSON text.
*/
template <typename F> QJsonObject via_c_json(F &&to_json)
{
const int size = to_json(nullptr, 0);
QByteArray buf(size + 1, '\0');
to_json(buf.data(), size + 1);
buf.resize(size);
return QJsonDocument::fromJson(buf).object();
}
inline QByteArray compact_json(const QJsonObject &o)
{
return QJsonDocument(o).toJson(QJsonDocument::Compact);
}
} // namespace detail
/**
* @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.
*/
inline bool write_message(QIODevice *device, const QJsonObject &obj)
{
QByteArray line = detail::compact_json(obj);
line.append('\n');
return device->write(line) == line.size();
}
/**
* @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.
*/
inline bool read_message(QByteArray *buffer, QJsonObject *out, bool *ok = nullptr)
{
while (true) {
const int newline = buffer->indexOf('\n');
if (newline < 0) {
// No complete line buffered yet.
return false;
}
const QByteArray line = buffer->left(newline);
buffer->remove(0, newline + 1);
// Skip blank lines silently (e.g. a stray newline) without flagging an error.
if (line.trimmed().isEmpty()) {
continue;
}
QJsonParseError err;
const QJsonDocument doc = QJsonDocument::fromJson(line, &err);
if (err.error != QJsonParseError::NoError || !doc.isObject()) {
if (ok) {
*ok = false;
}
return false;
}
*out = doc.object();
if (ok) {
*ok = true;
}
return true;
}
}
// ---- Typed message builders / parsers -------------------------------------------------------
//
// Thin wrappers that convert each struct to/from the C ABI POD form and let the library build or
// read the JSON, 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
{
oak_ipc_handshake c;
c.protocol_version = protocol_version;
detail::copy_str(shm_key, c.shm_key, sizeof(c.shm_key));
detail::copy_str(input_shm_key, c.input_shm_key,
sizeof(c.input_shm_key));
c.input_slots = input_slots;
c.output_slots = output_slots;
c.slot_data_bytes = slot_data_bytes;
c.input_slot_data_bytes = input_slot_data_bytes;
return detail::via_c_json([&](char *buf, int size) {
return oakengine_ipc_handshake_to_json(&c, buf, size);
});
}
static bool from_json(const QJsonObject &o, HandshakeMsg *out)
{
oak_ipc_handshake c;
if (!oakengine_ipc_handshake_parse(detail::compact_json(o).constData(),
&c)) {
return false;
}
out->protocol_version = c.protocol_version;
out->shm_key = QString::fromUtf8(c.shm_key);
out->input_shm_key = QString::fromUtf8(c.input_shm_key);
out->input_slots = c.input_slots;
out->output_slots = c.output_slots;
out->slot_data_bytes = c.slot_data_bytes;
out->input_slot_data_bytes = c.input_slot_data_bytes;
return true;
}
};
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
{
oak_ipc_render_frame c;
c.ticket_id = ticket_id;
detail::copy_str(node_uuid, c.node_uuid, sizeof(c.node_uuid));
c.time_num = time_num;
c.time_den = time_den;
c.width = width;
c.height = height;
c.format = format;
c.channel_count = channel_count;
c.mode = mode;
c.input_slot = input_slot;
c.input_slot_count = std::min(int(input_slots.size()),
OAK_IPC_INPUT_SLOTS_CAP);
for (int i = 0; i < c.input_slot_count; i++) {
c.input_slots[i] = input_slots.at(i);
}
c.has_color_transform = has_color_transform ? 1 : 0;
c.color_is_display = color_is_display ? 1 : 0;
detail::copy_str(color_output, c.color_output,
sizeof(c.color_output));
detail::copy_str(color_view, c.color_view, sizeof(c.color_view));
detail::copy_str(color_look, c.color_look, sizeof(c.color_look));
return detail::via_c_json([&](char *buf, int size) {
return oakengine_ipc_render_frame_to_json(&c, buf, size);
});
}
static bool from_json(const QJsonObject &o, RenderFrameMsg *out)
{
oak_ipc_render_frame c;
if (!oakengine_ipc_render_frame_parse(
detail::compact_json(o).constData(), &c)) {
return false;
}
out->ticket_id = c.ticket_id;
out->node_uuid = QString::fromUtf8(c.node_uuid);
out->time_num = c.time_num;
out->time_den = c.time_den;
out->width = c.width;
out->height = c.height;
out->format = c.format;
out->channel_count = c.channel_count;
out->mode = c.mode;
out->input_slot = c.input_slot;
out->input_slots.clear();
for (int i = 0; i < c.input_slot_count; i++) {
out->input_slots.append(c.input_slots[i]);
}
out->has_color_transform = c.has_color_transform != 0;
out->color_is_display = c.color_is_display != 0;
out->color_output = QString::fromUtf8(c.color_output);
out->color_view = QString::fromUtf8(c.color_view);
out->color_look = QString::fromUtf8(c.color_look);
return true;
}
};
struct FrameReadyMsg {
qint64 ticket_id = 0;
int output_slot = 0; ///< Index into the worker->main output FrameSlotPool.
QJsonObject to_json() const
{
oak_ipc_frame_ready c;
c.ticket_id = ticket_id;
c.output_slot = output_slot;
return detail::via_c_json([&](char *buf, int size) {
return oakengine_ipc_frame_ready_to_json(&c, buf, size);
});
}
static bool from_json(const QJsonObject &o, FrameReadyMsg *out)
{
oak_ipc_frame_ready c;
if (!oakengine_ipc_frame_ready_parse(
detail::compact_json(o).constData(), &c)) {
return false;
}
out->ticket_id = c.ticket_id;
out->output_slot = c.output_slot;
return true;
}
};
struct CancelMsg {
qint64 ticket_id = 0;
QJsonObject to_json() const
{
oak_ipc_cancel c;
c.ticket_id = ticket_id;
return detail::via_c_json([&](char *buf, int size) {
return oakengine_ipc_cancel_to_json(&c, buf, size);
});
}
static bool from_json(const QJsonObject &o, CancelMsg *out)
{
oak_ipc_cancel c;
if (!oakengine_ipc_cancel_parse(detail::compact_json(o).constData(),
&c)) {
return false;
}
out->ticket_id = c.ticket_id;
return true;
}
};
struct LoadGraphMsg {
QString path; ///< Temporary file holding the serialized node graph.
QJsonObject to_json() const
{
oak_ipc_load_graph c;
detail::copy_str(path, c.path, sizeof(c.path));
return detail::via_c_json([&](char *buf, int size) {
return oakengine_ipc_load_graph_to_json(&c, buf, size);
});
}
static bool from_json(const QJsonObject &o, LoadGraphMsg *out)
{
oak_ipc_load_graph c;
if (!oakengine_ipc_load_graph_parse(detail::compact_json(o).constData(),
&c)) {
return false;
}
out->path = QString::fromUtf8(c.path);
return true;
}
};
} // namespace ipc
} // namespace olive
#endif // OAK_IPC_IPCMESSAGE_H