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:
+35
-2
@@ -60,16 +60,26 @@ set_target_properties(oakengine PROPERTIES
|
||||
)
|
||||
|
||||
# Consumers resolve engine headers ("node/...", "render/...", "coreengine.h",
|
||||
# "ui/icons/icons.h", "tool/tool.h") from the engine root
|
||||
# "ui/icons/icons.h", "tool/tool.h") from the engine root, and the public C
|
||||
# API ("oakengine/ipc.h") from include/. The library itself builds against its
|
||||
# internal implementation headers under src/oliveimpl, included with an
|
||||
# "oliveimpl/"-prefixed path resolved from src/ (mirrors the src/oliveimpl
|
||||
# comment in core/CMakeLists.txt). A plain include-order trick like core's
|
||||
# cannot work here: CMAKE_INCLUDE_CURRENT_DIR puts the engine root ahead of
|
||||
# every target-level directory, so the prefixed paths are what keep internal
|
||||
# sources from ever picking up a consumer wrapper header by accident.
|
||||
target_include_directories(oakengine
|
||||
PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
|
||||
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
PRIVATE ${CMAKE_SOURCE_DIR}/third_party/openfx/include
|
||||
${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include
|
||||
${OLIVE_INCLUDE_DIRS}
|
||||
)
|
||||
|
||||
target_link_libraries(oakengine PUBLIC ${OLIVE_LIBRARIES} OfxHost)
|
||||
target_compile_definitions(oakengine PRIVATE ${OLIVE_DEFINITIONS})
|
||||
# OAKENGINE_BUILD marks the library side of the C ABI export macros (dllexport)
|
||||
target_compile_definitions(oakengine PRIVATE ${OLIVE_DEFINITIONS} OAKENGINE_BUILD)
|
||||
target_compile_options(oakengine PRIVATE ${OLIVE_COMPILE_OPTIONS})
|
||||
|
||||
# Install into the platform's standard library directory (/usr/lib,
|
||||
@@ -193,3 +203,26 @@ if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND)
|
||||
target_compile_options(oakvulkan-cabi-check PRIVATE ${OLIVE_COMPILE_OPTIONS})
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
# Pure C ABI tests for the liboakengine public interface (mirrors the
|
||||
# make_test pattern in core/CMakeLists.txt). They link only against oakengine
|
||||
# and must not depend on OpenGL/Vulkan.
|
||||
if (BUILD_TESTS)
|
||||
enable_testing()
|
||||
|
||||
function(make_oakengine_test name)
|
||||
# oakengine leaves olive::k_app_version to the final executable; the
|
||||
# version object library provides it (same as worker/CMakeLists.txt).
|
||||
add_executable(${name}
|
||||
tests/${name}.cpp
|
||||
$<TARGET_OBJECTS:olive-version-obj>
|
||||
)
|
||||
target_link_libraries(${name} PRIVATE oakengine)
|
||||
target_include_directories(${name} PRIVATE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/include"
|
||||
)
|
||||
add_test(${name} ${name})
|
||||
endfunction()
|
||||
|
||||
make_oakengine_test(oakengine_ipc_test)
|
||||
endif ()
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/***
|
||||
|
||||
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_EXPORT_H
|
||||
#define OAKENGINE_EXPORT_H
|
||||
|
||||
/**
|
||||
* @file export.h
|
||||
* @brief Symbol visibility macros for liboakengine
|
||||
*
|
||||
* liboakengine is transitioning to a pure C ABI: every public C function is
|
||||
* declared with OAKENGINE_API. While the migration is in flight the library
|
||||
* is still built with default symbol visibility, so the legacy C++ symbols
|
||||
* remain exported alongside the C ABI.
|
||||
*/
|
||||
#if defined(_WIN32) || defined(__CYGWIN__)
|
||||
#ifdef OAKENGINE_BUILD
|
||||
#define OAKENGINE_API __declspec(dllexport)
|
||||
#else
|
||||
#define OAKENGINE_API __declspec(dllimport)
|
||||
#endif
|
||||
#elif defined(__GNUC__) || defined(__clang__)
|
||||
#define OAKENGINE_API __attribute__((visibility("default")))
|
||||
#else
|
||||
#define OAKENGINE_API
|
||||
#endif
|
||||
|
||||
#endif /* OAKENGINE_EXPORT_H */
|
||||
@@ -0,0 +1,431 @@
|
||||
/***
|
||||
|
||||
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 */
|
||||
@@ -14,14 +14,26 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# The IPC subsystem is migrating behind the liboakengine C ABI facade:
|
||||
# - include/oakengine/*.h public C API (+ the inline SPSC header)
|
||||
# - src/oliveimpl/render/ipc/*.h internal implementation headers
|
||||
# - src/capi/ipc.cpp C ABI implementation
|
||||
# - render/ipc/*.{h,cpp} implementation sources and the
|
||||
# consumer-side C++ wrapper headers
|
||||
set(OLIVE_SOURCES
|
||||
${OLIVE_SOURCES}
|
||||
include/oakengine/export.h
|
||||
include/oakengine/ipc.h
|
||||
include/oakengine/spscringbuffer.h
|
||||
src/capi/ipc.cpp
|
||||
src/oliveimpl/render/ipc/frameslotpool.h
|
||||
src/oliveimpl/render/ipc/ipcmessage.h
|
||||
src/oliveimpl/render/ipc/sharedmemoryregion.h
|
||||
render/ipc/frameslotpool.cpp
|
||||
render/ipc/frameslotpool.h
|
||||
render/ipc/ipcmessage.cpp
|
||||
render/ipc/ipcmessage.h
|
||||
render/ipc/sharedmemoryregion.cpp
|
||||
render/ipc/sharedmemoryregion.h
|
||||
render/ipc/spscringbuffer.h
|
||||
PARENT_SCOPE
|
||||
)
|
||||
@@ -18,12 +18,16 @@
|
||||
|
||||
***/
|
||||
|
||||
#include "frameslotpool.h"
|
||||
#include "oliveimpl/render/ipc/frameslotpool.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace engine
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
@@ -45,9 +49,9 @@ size_t FrameSlotPool::bytes_needed(uint32_t slot_count, size_t slot_data_bytes)
|
||||
const uint32_t ring_cap = ring_capacity(slot_count);
|
||||
size_t total = align_up(sizeof(Header), k_align);
|
||||
total +=
|
||||
align_up(SpscRingBuffer::bytes_needed(ring_cap), k_align); // free ring
|
||||
align_up(olive::ipc::SpscRingBuffer::bytes_needed(ring_cap), k_align); // free ring
|
||||
total +=
|
||||
align_up(SpscRingBuffer::bytes_needed(ring_cap), k_align); // ready ring
|
||||
align_up(olive::ipc::SpscRingBuffer::bytes_needed(ring_cap), k_align); // ready ring
|
||||
total +=
|
||||
align_up(sizeof(FrameSlotMeta) * slot_count, k_align); // metadata array
|
||||
total += align_up(slot_data_bytes, k_align) * slot_count; // pixel data blocks
|
||||
@@ -67,10 +71,10 @@ FrameSlotPool FrameSlotPool::create(void *mem, uint32_t slot_count,
|
||||
offset += align_up(sizeof(Header), k_align);
|
||||
|
||||
const size_t free_off = offset;
|
||||
offset += align_up(SpscRingBuffer::bytes_needed(ring_cap), k_align);
|
||||
offset += align_up(olive::ipc::SpscRingBuffer::bytes_needed(ring_cap), k_align);
|
||||
|
||||
const size_t ready_off = offset;
|
||||
offset += align_up(SpscRingBuffer::bytes_needed(ring_cap), k_align);
|
||||
offset += align_up(olive::ipc::SpscRingBuffer::bytes_needed(ring_cap), k_align);
|
||||
|
||||
const size_t meta_off = offset;
|
||||
offset += align_up(sizeof(FrameSlotMeta) * slot_count, k_align);
|
||||
@@ -86,8 +90,8 @@ FrameSlotPool FrameSlotPool::create(void *mem, uint32_t slot_count,
|
||||
pool.header_->meta_offset = meta_off;
|
||||
pool.header_->data_offset = data_off;
|
||||
|
||||
pool.free_ring_ = SpscRingBuffer::create(pool.base_ + free_off, ring_cap);
|
||||
pool.ready_ring_ = SpscRingBuffer::create(pool.base_ + ready_off, ring_cap);
|
||||
pool.free_ring_ = olive::ipc::SpscRingBuffer::create(pool.base_ + free_off, ring_cap);
|
||||
pool.ready_ring_ = olive::ipc::SpscRingBuffer::create(pool.base_ + ready_off, ring_cap);
|
||||
pool.meta_ = reinterpret_cast<FrameSlotMeta *>(pool.base_ + meta_off);
|
||||
pool.data_ = pool.base_ + data_off;
|
||||
|
||||
@@ -115,9 +119,9 @@ FrameSlotPool FrameSlotPool::attach(void *mem)
|
||||
}
|
||||
|
||||
pool.free_ring_ =
|
||||
SpscRingBuffer::attach(pool.base_ + pool.header_->free_ring_offset);
|
||||
olive::ipc::SpscRingBuffer::attach(pool.base_ + pool.header_->free_ring_offset);
|
||||
pool.ready_ring_ =
|
||||
SpscRingBuffer::attach(pool.base_ + pool.header_->ready_ring_offset);
|
||||
olive::ipc::SpscRingBuffer::attach(pool.base_ + pool.header_->ready_ring_offset);
|
||||
pool.meta_ = reinterpret_cast<FrameSlotMeta *>(pool.base_ +
|
||||
pool.header_->meta_offset);
|
||||
pool.data_ = pool.base_ + pool.header_->data_offset;
|
||||
@@ -176,4 +180,6 @@ bool FrameSlotPool::release(uint32_t index)
|
||||
}
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace internal
|
||||
} // namespace engine
|
||||
} // namespace olive
|
||||
@@ -24,7 +24,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "spscringbuffer.h"
|
||||
#include "oakengine/ipc.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
@@ -34,147 +34,193 @@ 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).
|
||||
* Trivially-copyable POD that lives in shared memory alongside the pixel data, part of the
|
||||
* version-1 wire protocol with the render worker. This is the C ABI oak_frame_slot_meta struct,
|
||||
* aliased so the shared-memory layout is defined exactly once, in oakengine/ipc.h.
|
||||
*/
|
||||
struct FrameSlotMeta {
|
||||
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; ///< olive::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[128]; ///< Input colorspace name for color-managed footage.
|
||||
};
|
||||
typedef oak_frame_slot_meta FrameSlotMeta;
|
||||
|
||||
/**
|
||||
* @brief A fixed-size pool of equal-sized frame slots in shared memory, with lock-free hand-off.
|
||||
*
|
||||
* Consumer-side wrapper over the liboakengine C ABI: the object only holds an opaque
|
||||
* OakFrameSlotPool handle and forwards every call across the C boundary. The public API is
|
||||
* unchanged from the original implementation; see oakengine/ipc.h for the protocol description.
|
||||
*
|
||||
* 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.
|
||||
* main -> worker for decoded input). The pool does NOT own the memory; it is constructed over a
|
||||
* SharedMemoryRegion mapping. Use bytes_needed() to size that region.
|
||||
*/
|
||||
class FrameSlotPool {
|
||||
public:
|
||||
FrameSlotPool() = default;
|
||||
|
||||
FrameSlotPool(const FrameSlotPool &rhs)
|
||||
: handle_(oakengine_ipc_framepool_copy(rhs.handle_))
|
||||
{
|
||||
}
|
||||
|
||||
FrameSlotPool(FrameSlotPool &&rhs) noexcept
|
||||
: handle_(rhs.handle_)
|
||||
{
|
||||
rhs.handle_ = nullptr;
|
||||
}
|
||||
|
||||
~FrameSlotPool()
|
||||
{
|
||||
oakengine_ipc_framepool_free(handle_);
|
||||
}
|
||||
|
||||
FrameSlotPool &operator=(const FrameSlotPool &rhs)
|
||||
{
|
||||
if (this != &rhs) {
|
||||
oakengine_ipc_framepool_free(handle_);
|
||||
handle_ = oakengine_ipc_framepool_copy(rhs.handle_);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
FrameSlotPool &operator=(FrameSlotPool &&rhs) noexcept
|
||||
{
|
||||
if (this != &rhs) {
|
||||
oakengine_ipc_framepool_free(handle_);
|
||||
handle_ = rhs.handle_;
|
||||
rhs.handle_ = nullptr;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
static size_t bytes_needed(uint32_t slot_count, size_t slot_data_bytes)
|
||||
{
|
||||
return oakengine_ipc_framepool_bytes_needed(slot_count, 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.
|
||||
* `mem` must provide at least bytes_needed(slot_count, slot_data_bytes) bytes.
|
||||
*/
|
||||
static FrameSlotPool create(void *mem, uint32_t slot_count,
|
||||
size_t slot_data_bytes);
|
||||
size_t slot_data_bytes)
|
||||
{
|
||||
return from_handle(oakengine_ipc_framepool_create(mem, slot_count,
|
||||
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().
|
||||
* Reads slot_count/slot_data_bytes from the in-memory header written by create().
|
||||
*/
|
||||
static FrameSlotPool attach(void *mem);
|
||||
static FrameSlotPool attach(void *mem)
|
||||
{
|
||||
return from_handle(oakengine_ipc_framepool_attach(mem));
|
||||
}
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return header_ != nullptr;
|
||||
return oakengine_ipc_framepool_is_valid(handle_) != 0;
|
||||
}
|
||||
|
||||
uint32_t slot_count() const;
|
||||
size_t slot_data_bytes() const;
|
||||
uint32_t slot_count() const
|
||||
{
|
||||
return oakengine_ipc_framepool_slot_count(handle_);
|
||||
}
|
||||
|
||||
size_t slot_data_bytes() const
|
||||
{
|
||||
return oakengine_ipc_framepool_slot_data_bytes(handle_);
|
||||
}
|
||||
|
||||
// ---- Filler side ----
|
||||
|
||||
/**
|
||||
* @brief Take ownership of a free slot. Returns false (and leaves *index untouched) if none free.
|
||||
*/
|
||||
bool acquire(uint32_t *index);
|
||||
bool acquire(uint32_t *index)
|
||||
{
|
||||
return oakengine_ipc_framepool_acquire(handle_, index) != 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Pointer to a slot's pixel data block (slot_data_bytes available).
|
||||
*/
|
||||
void *slot_data(uint32_t index);
|
||||
void *slot_data(uint32_t index)
|
||||
{
|
||||
return oakengine_ipc_framepool_slot_data(handle_, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Mutable metadata for a slot. Filler writes this before Publish().
|
||||
* @brief Mutable metadata for a slot. Filler writes this before publish().
|
||||
*/
|
||||
FrameSlotMeta *meta(uint32_t index);
|
||||
FrameSlotMeta *meta(uint32_t index)
|
||||
{
|
||||
return oakengine_ipc_framepool_meta(handle_, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Publish a filled slot to the drainer. Must follow a successful Acquire() of `index`.
|
||||
* @brief Publish a filled slot to the drainer. Must follow a successful acquire() of `index`.
|
||||
*/
|
||||
bool publish(uint32_t index);
|
||||
bool publish(uint32_t index)
|
||||
{
|
||||
return oakengine_ipc_framepool_publish(handle_, index) != 0;
|
||||
}
|
||||
|
||||
// ---- 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)
|
||||
bool consume(uint32_t *index)
|
||||
{
|
||||
return slot_count + 1;
|
||||
return oakengine_ipc_framepool_consume(handle_, index) != 0;
|
||||
}
|
||||
|
||||
uint8_t *base_ = nullptr;
|
||||
Header *header_ = nullptr;
|
||||
SpscRingBuffer *free_ring_ = nullptr;
|
||||
SpscRingBuffer *ready_ring_ = nullptr;
|
||||
FrameSlotMeta *meta_ = nullptr;
|
||||
uint8_t *data_ = nullptr;
|
||||
/**
|
||||
* @brief Return a consumed slot to the free pool for reuse. Must follow consume() of `index`.
|
||||
*/
|
||||
bool release(uint32_t index)
|
||||
{
|
||||
return oakengine_ipc_framepool_release(handle_, index) != 0;
|
||||
}
|
||||
|
||||
const FrameSlotMeta *meta(uint32_t index) const
|
||||
{
|
||||
return oakengine_ipc_framepool_meta_const(handle_, index);
|
||||
}
|
||||
|
||||
const void *slot_data(uint32_t index) const
|
||||
{
|
||||
return oakengine_ipc_framepool_slot_data_const(handle_, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The wrapped C handle, for cross-type wrappers and direct C API use
|
||||
*/
|
||||
OakFrameSlotPool *handle() const
|
||||
{
|
||||
return handle_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Wraps an owned C handle (takes ownership)
|
||||
*/
|
||||
static FrameSlotPool from_handle(OakFrameSlotPool *handle)
|
||||
{
|
||||
return FrameSlotPool(handle);
|
||||
}
|
||||
|
||||
private:
|
||||
explicit FrameSlotPool(OakFrameSlotPool *handle)
|
||||
: handle_(handle)
|
||||
{
|
||||
}
|
||||
|
||||
OakFrameSlotPool *handle_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
***/
|
||||
|
||||
#include "ipcmessage.h"
|
||||
#include "oliveimpl/render/ipc/ipcmessage.h"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
@@ -26,6 +26,10 @@
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace engine
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
@@ -227,4 +231,6 @@ bool LoadGraphMsg::from_json(const QJsonObject &o, LoadGraphMsg *out)
|
||||
}
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace internal
|
||||
} // namespace engine
|
||||
} // namespace olive
|
||||
|
||||
+238
-23
@@ -21,12 +21,17 @@
|
||||
#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
|
||||
@@ -43,6 +48,10 @@ namespace ipc
|
||||
* 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.
|
||||
@@ -55,23 +64,58 @@ namespace ipc
|
||||
*/
|
||||
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";
|
||||
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.
|
||||
*/
|
||||
bool write_message(QIODevice *device, const QJsonObject &obj);
|
||||
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.
|
||||
@@ -82,13 +126,46 @@ bool write_message(QIODevice *device, const QJsonObject &obj);
|
||||
* 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);
|
||||
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 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).
|
||||
// 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;
|
||||
@@ -100,8 +177,38 @@ struct HandshakeMsg {
|
||||
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);
|
||||
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 {
|
||||
@@ -129,30 +236,138 @@ struct RenderFrameMsg {
|
||||
QString color_view;
|
||||
QString color_look;
|
||||
|
||||
QJsonObject to_json() const;
|
||||
static bool from_json(const QJsonObject &o, RenderFrameMsg *out);
|
||||
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;
|
||||
static bool from_json(const QJsonObject &o, FrameReadyMsg *out);
|
||||
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;
|
||||
static bool from_json(const QJsonObject &o, CancelMsg *out);
|
||||
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;
|
||||
static bool from_json(const QJsonObject &o, LoadGraphMsg *out);
|
||||
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
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
***/
|
||||
|
||||
#include "sharedmemoryregion.h"
|
||||
#include "oliveimpl/render/ipc/sharedmemoryregion.h"
|
||||
|
||||
#include <QtGlobal>
|
||||
|
||||
@@ -35,6 +35,10 @@
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace engine
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
namespace ipc
|
||||
{
|
||||
|
||||
@@ -228,4 +232,6 @@ void SharedMemoryRegion::close()
|
||||
#endif
|
||||
|
||||
} // namespace ipc
|
||||
} // namespace internal
|
||||
} // namespace engine
|
||||
} // namespace olive
|
||||
@@ -24,6 +24,8 @@
|
||||
#include <cstddef>
|
||||
#include <QString>
|
||||
|
||||
#include "oakengine/ipc.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
namespace ipc
|
||||
@@ -32,27 +34,34 @@ 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.
|
||||
* Consumer-side wrapper over the liboakengine C ABI: the object only holds an opaque
|
||||
* OakSharedMemoryRegion handle and forwards every call across the C boundary. The public API is
|
||||
* unchanged from the original implementation.
|
||||
*
|
||||
* 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.
|
||||
* One process open()s the segment with k_create (owner); the peer process open()s it by the same
|
||||
* key with k_attach. 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.
|
||||
*/
|
||||
class SharedMemoryRegion {
|
||||
public:
|
||||
enum Mode {
|
||||
/// Create (and own) the segment. Fails if it already exists; unlinks on destruction.
|
||||
k_create,
|
||||
k_create = OAK_IPC_SHM_MODE_CREATE,
|
||||
/// Attach to a segment created by the peer. Does not unlink on destruction.
|
||||
k_attach
|
||||
k_attach = OAK_IPC_SHM_MODE_ATTACH
|
||||
};
|
||||
|
||||
SharedMemoryRegion();
|
||||
~SharedMemoryRegion();
|
||||
SharedMemoryRegion()
|
||||
: handle_(oakengine_ipc_shm_create())
|
||||
{
|
||||
}
|
||||
|
||||
~SharedMemoryRegion()
|
||||
{
|
||||
oakengine_ipc_shm_free(handle_);
|
||||
}
|
||||
|
||||
SharedMemoryRegion(const SharedMemoryRegion &) = delete;
|
||||
SharedMemoryRegion &operator=(const SharedMemoryRegion &) = delete;
|
||||
@@ -63,26 +72,36 @@ public:
|
||||
* `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);
|
||||
bool open(const QString &key, size_t size, Mode mode)
|
||||
{
|
||||
const bool ok = oakengine_ipc_shm_open(
|
||||
handle_, key.toUtf8().constData(), size,
|
||||
static_cast<oak_ipc_shm_mode>(mode)) != 0;
|
||||
refresh_caches();
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Unmap and (if owner) unlink the segment. Called automatically by the destructor.
|
||||
*/
|
||||
void close();
|
||||
void close()
|
||||
{
|
||||
oakengine_ipc_shm_close(handle_);
|
||||
}
|
||||
|
||||
bool is_valid() const
|
||||
{
|
||||
return data_ != nullptr;
|
||||
return oakengine_ipc_shm_is_valid(handle_) != 0;
|
||||
}
|
||||
|
||||
void *data() const
|
||||
{
|
||||
return data_;
|
||||
return oakengine_ipc_shm_data(handle_);
|
||||
}
|
||||
|
||||
size_t size() const
|
||||
{
|
||||
return size_;
|
||||
return oakengine_ipc_shm_size(handle_);
|
||||
}
|
||||
|
||||
const QString &key() const
|
||||
@@ -100,21 +119,47 @@ public:
|
||||
*
|
||||
* Centralized so the owner and the spawned worker agree on the same name.
|
||||
*/
|
||||
static QString make_key(qint64 owner_pid, int worker_index);
|
||||
static QString make_key(qint64 owner_pid, int worker_index)
|
||||
{
|
||||
const int size = oakengine_ipc_shm_make_key(owner_pid, worker_index,
|
||||
nullptr, 0);
|
||||
QByteArray buf(size + 1, '\0');
|
||||
oakengine_ipc_shm_make_key(owner_pid, worker_index, buf.data(),
|
||||
size + 1);
|
||||
return QString::fromUtf8(buf.constData());
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The wrapped C handle, for cross-type wrappers and direct C API use
|
||||
*/
|
||||
OakSharedMemoryRegion *handle() const
|
||||
{
|
||||
return handle_;
|
||||
}
|
||||
|
||||
private:
|
||||
QString key_;
|
||||
size_t size_;
|
||||
void *data_;
|
||||
Mode mode_;
|
||||
QString error_;
|
||||
static QString query_string(int (*query)(const OakSharedMemoryRegion *,
|
||||
char *, int),
|
||||
const OakSharedMemoryRegion *handle)
|
||||
{
|
||||
const int size = query(handle, nullptr, 0);
|
||||
if (size <= 0) {
|
||||
return QString();
|
||||
}
|
||||
QByteArray buf(size + 1, '\0');
|
||||
query(handle, buf.data(), size + 1);
|
||||
return QString::fromUtf8(buf.constData());
|
||||
}
|
||||
|
||||
#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
|
||||
void refresh_caches()
|
||||
{
|
||||
key_ = query_string(oakengine_ipc_shm_key, handle_);
|
||||
error_ = query_string(oakengine_ipc_shm_error, handle_);
|
||||
}
|
||||
|
||||
OakSharedMemoryRegion *handle_;
|
||||
QString key_;
|
||||
QString error_;
|
||||
};
|
||||
|
||||
} // namespace ipc
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
#include "render/plugin/pluginrenderer.h"
|
||||
#include "pluginSupport/oliveclip.h"
|
||||
#include "pluginSupport/olivehost.h"
|
||||
#include "render/ipc/frameslotpool.h"
|
||||
#include "oliveimpl/render/ipc/frameslotpool.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
@@ -456,7 +456,7 @@ void RenderProcessor::process_video_footage(TexturePtr destination,
|
||||
render_ctx_->flush();
|
||||
};
|
||||
|
||||
auto *input_pool = QtUtils::value_to_ptr<ipc::FrameSlotPool>(
|
||||
auto *input_pool = QtUtils::value_to_ptr<engine::internal::ipc::FrameSlotPool>(
|
||||
ticket_->property("ipc_input_pool"));
|
||||
int input_slot = -1;
|
||||
const QVariantList input_slots =
|
||||
@@ -481,7 +481,7 @@ void RenderProcessor::process_video_footage(TexturePtr destination,
|
||||
return;
|
||||
}
|
||||
|
||||
const ipc::FrameSlotMeta *meta = input_pool->meta(uint32_t(input_slot));
|
||||
const engine::internal::ipc::FrameSlotMeta *meta = input_pool->meta(uint32_t(input_slot));
|
||||
if (meta && meta->width > 0 && meta->height > 0 &&
|
||||
meta->data_size > 0 &&
|
||||
meta->data_size <= int(input_pool->slot_data_bytes())) {
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
#include "common/qtutils.h"
|
||||
#include "node/project/footage/footage.h"
|
||||
#include "node/traverser.h"
|
||||
#include "oliveimpl/render/ipc/frameslotpool.h"
|
||||
#include "oliveimpl/render/ipc/ipcmessage.h"
|
||||
#include "oliveimpl/render/ipc/sharedmemoryregion.h"
|
||||
|
||||
namespace olive
|
||||
{
|
||||
@@ -381,7 +384,7 @@ bool read_control_message(QProcess *process, QJsonObject *out, QString *error,
|
||||
|
||||
*out = doc.object();
|
||||
if (out->value(QStringLiteral("type")).toString() ==
|
||||
QLatin1String(ipc::msgtype::k_error)) {
|
||||
QLatin1String(engine::internal::ipc::msgtype::k_error)) {
|
||||
if (error) {
|
||||
*error = out->value(QStringLiteral("message")).toString();
|
||||
}
|
||||
@@ -398,6 +401,28 @@ bool read_control_message(QProcess *process, QJsonObject *out, QString *error,
|
||||
|
||||
} // namespace
|
||||
|
||||
// Holds the persistent per-worker IPC state. Defined here rather than in the
|
||||
// header because the member types are engine-internal (oliveimpl): the header
|
||||
// is consumed outside the library and only forward-declares this struct.
|
||||
struct RenderWorkerPool::PooledWorker {
|
||||
QProcess *process = nullptr;
|
||||
QString loaded_graph_path;
|
||||
qint64 last_used_ms = 0;
|
||||
int use_count = 0;
|
||||
|
||||
// Persistent shared memory for this worker. Reusing regions across frames
|
||||
// avoids the cost of creating/destroying large shm segments every render.
|
||||
engine::internal::ipc::SharedMemoryRegion output_region;
|
||||
engine::internal::ipc::FrameSlotPool output_pool;
|
||||
size_t output_slot_bytes = 0;
|
||||
QString output_shm_key;
|
||||
|
||||
engine::internal::ipc::SharedMemoryRegion input_region;
|
||||
engine::internal::ipc::FrameSlotPool input_pool;
|
||||
size_t input_slot_bytes = 0;
|
||||
QString input_shm_key;
|
||||
};
|
||||
|
||||
RenderWorkerPool::RenderWorkerPool(DecoderCache *decoder_cache,
|
||||
const QString &gpu_backend, QObject *parent)
|
||||
: QThread(parent)
|
||||
@@ -782,33 +807,33 @@ RenderWorkerPool::process_job_attempt(const Job &job, int worker_index,
|
||||
}
|
||||
}
|
||||
const size_t output_region_bytes =
|
||||
ipc::FrameSlotPool::bytes_needed(k_output_slots, output_slot_bytes);
|
||||
engine::internal::ipc::FrameSlotPool::bytes_needed(k_output_slots, output_slot_bytes);
|
||||
|
||||
if (!worker->output_region.is_valid() ||
|
||||
worker->output_slot_bytes < output_slot_bytes) {
|
||||
if (worker->output_region.is_valid()) {
|
||||
worker->output_region.close();
|
||||
worker->output_pool = ipc::FrameSlotPool();
|
||||
worker->output_pool = engine::internal::ipc::FrameSlotPool();
|
||||
}
|
||||
if (worker->output_shm_key.isEmpty()) {
|
||||
worker->output_shm_key =
|
||||
ipc::SharedMemoryRegion::make_key(worker_process_id, 0) +
|
||||
engine::internal::ipc::SharedMemoryRegion::make_key(worker_process_id, 0) +
|
||||
QStringLiteral("-out");
|
||||
}
|
||||
if (!worker->output_region.open(worker->output_shm_key,
|
||||
output_region_bytes,
|
||||
ipc::SharedMemoryRegion::k_create)) {
|
||||
engine::internal::ipc::SharedMemoryRegion::k_create)) {
|
||||
qWarning()
|
||||
<< "RenderWorkerPool failed to create output shared memory"
|
||||
<< worker->output_region.error();
|
||||
return JobResult::k_fatal_failure;
|
||||
}
|
||||
worker->output_pool = ipc::FrameSlotPool::create(
|
||||
worker->output_pool = engine::internal::ipc::FrameSlotPool::create(
|
||||
worker->output_region.data(), k_output_slots, output_slot_bytes);
|
||||
worker->output_slot_bytes = output_slot_bytes;
|
||||
}
|
||||
const QString shm_key = worker->output_shm_key;
|
||||
ipc::FrameSlotPool &output_pool = worker->output_pool;
|
||||
engine::internal::ipc::FrameSlotPool &output_pool = worker->output_pool;
|
||||
|
||||
const uint32_t input_slot_count =
|
||||
job.input_frames.isEmpty() ? 0 : uint32_t(job.input_frames.size());
|
||||
@@ -818,31 +843,31 @@ RenderWorkerPool::process_job_attempt(const Job &job, int worker_index,
|
||||
worker->input_pool.slot_count() < input_slot_count) {
|
||||
if (worker->input_region.is_valid()) {
|
||||
worker->input_region.close();
|
||||
worker->input_pool = ipc::FrameSlotPool();
|
||||
worker->input_pool = engine::internal::ipc::FrameSlotPool();
|
||||
}
|
||||
if (worker->input_shm_key.isEmpty()) {
|
||||
worker->input_shm_key =
|
||||
ipc::SharedMemoryRegion::make_key(worker_process_id, 1) +
|
||||
engine::internal::ipc::SharedMemoryRegion::make_key(worker_process_id, 1) +
|
||||
QStringLiteral("-in");
|
||||
}
|
||||
const size_t input_region_bytes = ipc::FrameSlotPool::bytes_needed(
|
||||
const size_t input_region_bytes = engine::internal::ipc::FrameSlotPool::bytes_needed(
|
||||
input_slot_count, input_slot_bytes);
|
||||
if (!worker->input_region.open(worker->input_shm_key,
|
||||
input_region_bytes,
|
||||
ipc::SharedMemoryRegion::k_create)) {
|
||||
engine::internal::ipc::SharedMemoryRegion::k_create)) {
|
||||
qWarning()
|
||||
<< "RenderWorkerPool failed to create input shared memory"
|
||||
<< worker->input_region.error();
|
||||
return JobResult::k_fatal_failure;
|
||||
}
|
||||
worker->input_pool =
|
||||
ipc::FrameSlotPool::create(worker->input_region.data(),
|
||||
engine::internal::ipc::FrameSlotPool::create(worker->input_region.data(),
|
||||
input_slot_count, input_slot_bytes);
|
||||
worker->input_slot_bytes = input_slot_bytes;
|
||||
}
|
||||
}
|
||||
const QString input_shm_key = worker->input_shm_key;
|
||||
ipc::FrameSlotPool &input_pool = worker->input_pool;
|
||||
engine::internal::ipc::FrameSlotPool &input_pool = worker->input_pool;
|
||||
QVector<int> input_slots;
|
||||
if (input_slot_count > 0) {
|
||||
for (const FramePtr &frame : job.input_frames) {
|
||||
@@ -860,7 +885,7 @@ RenderWorkerPool::process_job_attempt(const Job &job, int worker_index,
|
||||
|
||||
memcpy(input_pool.slot_data(slot), frame->const_data(),
|
||||
size_t(frame->allocated_size()));
|
||||
ipc::FrameSlotMeta *meta = input_pool.meta(slot);
|
||||
engine::internal::ipc::FrameSlotMeta *meta = input_pool.meta(slot);
|
||||
meta->id = qint64(input_slots.size());
|
||||
meta->time_num = frame->timestamp().numerator();
|
||||
meta->time_den = frame->timestamp().denominator();
|
||||
@@ -896,14 +921,14 @@ RenderWorkerPool::process_job_attempt(const Job &job, int worker_index,
|
||||
|
||||
set_active_worker(worker_index, job.ticket, worker->process, ticket_id);
|
||||
if (job.ticket->is_cancelled()) {
|
||||
ipc::CancelMsg cancel;
|
||||
engine::internal::ipc::CancelMsg cancel;
|
||||
cancel.ticket_id = ticket_id;
|
||||
try_write_control_message(worker->process, cancel.to_json());
|
||||
clear_active_worker(worker_index, worker_process_id);
|
||||
return JobResult::k_cancelled;
|
||||
}
|
||||
|
||||
ipc::HandshakeMsg handshake;
|
||||
engine::internal::ipc::HandshakeMsg handshake;
|
||||
handshake.protocol_version = k_protocol_version;
|
||||
handshake.shm_key = shm_key;
|
||||
handshake.input_shm_key = input_slots.isEmpty() ? QString() : input_shm_key;
|
||||
@@ -923,7 +948,7 @@ RenderWorkerPool::process_job_attempt(const Job &job, int worker_index,
|
||||
}
|
||||
|
||||
if (worker->loaded_graph_path != job.graph_path) {
|
||||
ipc::LoadGraphMsg load;
|
||||
engine::internal::ipc::LoadGraphMsg load;
|
||||
load.path = job.graph_path;
|
||||
QString error;
|
||||
QJsonObject response;
|
||||
@@ -939,7 +964,7 @@ RenderWorkerPool::process_job_attempt(const Job &job, int worker_index,
|
||||
}
|
||||
worker->loaded_graph_path = job.graph_path;
|
||||
}
|
||||
ipc::RenderFrameMsg render;
|
||||
engine::internal::ipc::RenderFrameMsg render;
|
||||
render.ticket_id = ticket_id;
|
||||
render.node_uuid = job.node_token;
|
||||
render.time_num = job.params.time.numerator();
|
||||
@@ -970,7 +995,7 @@ RenderWorkerPool::process_job_attempt(const Job &job, int worker_index,
|
||||
|
||||
QString error;
|
||||
QJsonObject response;
|
||||
ipc::FrameReadyMsg ready;
|
||||
engine::internal::ipc::FrameReadyMsg ready;
|
||||
while (true) {
|
||||
if (!read_control_message(worker->process, &response, &error, 30000)) {
|
||||
if (!job.ticket->is_cancelled()) {
|
||||
@@ -982,7 +1007,7 @@ RenderWorkerPool::process_job_attempt(const Job &job, int worker_index,
|
||||
JobResult::k_retryable_failure;
|
||||
}
|
||||
|
||||
if (ipc::FrameReadyMsg::from_json(response, &ready)) {
|
||||
if (engine::internal::ipc::FrameReadyMsg::from_json(response, &ready)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1174,7 +1199,7 @@ void RenderWorkerPool::shutdown_worker(PooledWorker *worker)
|
||||
|
||||
if (process->state() == QProcess::Running) {
|
||||
QJsonObject shutdown;
|
||||
shutdown[QStringLiteral("type")] = ipc::msgtype::k_shutdown;
|
||||
shutdown[QStringLiteral("type")] = engine::internal::ipc::msgtype::k_shutdown;
|
||||
try_write_control_message(process, shutdown);
|
||||
process->closeWriteChannel();
|
||||
if (!process->waitForFinished(5000)) {
|
||||
@@ -1209,10 +1234,10 @@ void RenderWorkerPool::clear_graph_cache()
|
||||
}
|
||||
|
||||
void RenderWorkerPool::finish_with_frame(RenderTicketPtr ticket,
|
||||
const ipc::FrameSlotPool &pool,
|
||||
const engine::internal::ipc::FrameSlotPool &pool,
|
||||
uint32_t slot)
|
||||
{
|
||||
const ipc::FrameSlotMeta *meta = pool.meta(slot);
|
||||
const engine::internal::ipc::FrameSlotMeta *meta = pool.meta(slot);
|
||||
if (!meta || meta->data_size <= 0 ||
|
||||
meta->data_size > int(pool.slot_data_bytes())) {
|
||||
ticket->finish();
|
||||
|
||||
@@ -32,9 +32,6 @@
|
||||
|
||||
#include "codec/frame.h"
|
||||
#include "node/project/serializer/serializer.h"
|
||||
#include "render/ipc/frameslotpool.h"
|
||||
#include "render/ipc/ipcmessage.h"
|
||||
#include "render/ipc/sharedmemoryregion.h"
|
||||
#include "render/rendermanager.h"
|
||||
|
||||
class QProcess;
|
||||
@@ -42,6 +39,16 @@ class QProcess;
|
||||
namespace olive
|
||||
{
|
||||
|
||||
// The worker IPC implementation lives behind the liboakengine C ABI facade
|
||||
// (src/oliveimpl/render/ipc). This header is consumed outside the engine
|
||||
// library, so it can only forward-declare the internal types used by private
|
||||
// method signatures; PooledWorker (which holds IPC objects by value) is
|
||||
// defined in the .cpp for the same reason.
|
||||
namespace engine::internal::ipc
|
||||
{
|
||||
class FrameSlotPool;
|
||||
}
|
||||
|
||||
class Project;
|
||||
|
||||
class RenderWorkerPool : public QThread {
|
||||
@@ -90,24 +97,9 @@ private:
|
||||
qint64 ticket_id = 0;
|
||||
};
|
||||
|
||||
struct PooledWorker {
|
||||
QProcess *process = nullptr;
|
||||
QString loaded_graph_path;
|
||||
qint64 last_used_ms = 0;
|
||||
int use_count = 0;
|
||||
|
||||
// Persistent shared memory for this worker. Reusing regions across frames
|
||||
// avoids the cost of creating/destroying large shm segments every render.
|
||||
ipc::SharedMemoryRegion output_region;
|
||||
ipc::FrameSlotPool output_pool;
|
||||
size_t output_slot_bytes = 0;
|
||||
QString output_shm_key;
|
||||
|
||||
ipc::SharedMemoryRegion input_region;
|
||||
ipc::FrameSlotPool input_pool;
|
||||
size_t input_slot_bytes = 0;
|
||||
QString input_shm_key;
|
||||
};
|
||||
// Defined in the .cpp: holds the per-worker IPC shared-memory regions and
|
||||
// frame slot pools (internal oliveimpl types) by value.
|
||||
struct PooledWorker;
|
||||
|
||||
struct CachedGraph {
|
||||
QString path;
|
||||
@@ -124,7 +116,8 @@ private:
|
||||
std::vector<std::unique_ptr<PooledWorker>> *local_pool);
|
||||
JobResult process_job_attempt(const Job &job, int worker_index,
|
||||
int attempt_index, PooledWorker *worker);
|
||||
void finish_with_frame(RenderTicketPtr ticket, const ipc::FrameSlotPool &pool,
|
||||
void finish_with_frame(RenderTicketPtr ticket,
|
||||
const engine::internal::ipc::FrameSlotPool &pool,
|
||||
uint32_t slot);
|
||||
void cleanup_graph_file(const QString &path);
|
||||
void add_graph_path_ref(const QString &path);
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,591 @@
|
||||
/***
|
||||
|
||||
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/>.
|
||||
|
||||
***/
|
||||
|
||||
// Pure C ABI test for the liboakengine render/ipc facade. Exercises the
|
||||
// shared-memory region, the frame slot pool (including its version-1 wire
|
||||
// layout) and every control-plane message build/parse pair. No Qt, no GL.
|
||||
|
||||
#include <assert.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#if defined(_WIN32)
|
||||
#include <process.h>
|
||||
#define getpid _getpid
|
||||
#else
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
#include "oakengine/ipc.h"
|
||||
|
||||
// ---- Version-1 wire layout: oak_frame_slot_meta must never move ----------
|
||||
static_assert(sizeof(oak_frame_slot_meta) == 176, "FrameSlotMeta size");
|
||||
static_assert(offsetof(oak_frame_slot_meta, id) == 0, "id offset");
|
||||
static_assert(offsetof(oak_frame_slot_meta, time_num) == 8, "time_num offset");
|
||||
static_assert(offsetof(oak_frame_slot_meta, time_den) == 16, "time_den offset");
|
||||
static_assert(offsetof(oak_frame_slot_meta, width) == 24, "width offset");
|
||||
static_assert(offsetof(oak_frame_slot_meta, height) == 28, "height offset");
|
||||
static_assert(offsetof(oak_frame_slot_meta, format) == 32, "format offset");
|
||||
static_assert(offsetof(oak_frame_slot_meta, channel_count) == 36,
|
||||
"channel_count offset");
|
||||
static_assert(offsetof(oak_frame_slot_meta, linesize) == 40, "linesize offset");
|
||||
static_assert(offsetof(oak_frame_slot_meta, data_size) == 44,
|
||||
"data_size offset");
|
||||
static_assert(offsetof(oak_frame_slot_meta, colorspace) == 48,
|
||||
"colorspace offset");
|
||||
|
||||
static void assert_json_eq(const char *produced, const char *expected)
|
||||
{
|
||||
if (strcmp(produced, expected) != 0) {
|
||||
fprintf(stderr, "JSON mismatch:\n produced: %s\n expected: %s\n",
|
||||
produced, expected);
|
||||
assert(0);
|
||||
}
|
||||
}
|
||||
|
||||
static void test_shm(void)
|
||||
{
|
||||
// make_key: buf/size convention and exact format
|
||||
char key[OAK_IPC_SHM_KEY_CAP];
|
||||
const int64_t pid = getpid();
|
||||
int needed = oakengine_ipc_shm_make_key(pid, 0, NULL, 0);
|
||||
char expected[64];
|
||||
snprintf(expected, sizeof(expected), "olive-rw-%lld-0", (long long)pid);
|
||||
assert(needed == (int)strlen(expected));
|
||||
assert(oakengine_ipc_shm_make_key(pid, 0, key, sizeof(key)) == needed);
|
||||
assert(strcmp(key, expected) == 0);
|
||||
assert(oakengine_ipc_shm_make_key(pid, 0, key, 6) == needed); // truncated
|
||||
assert(strcmp(key, "olive") == 0);
|
||||
// A different worker index produces a different key.
|
||||
char key1[OAK_IPC_SHM_KEY_CAP];
|
||||
assert(oakengine_ipc_shm_make_key(pid, 1, key1, sizeof(key1)) == needed);
|
||||
assert(strcmp(key1, expected) != 0);
|
||||
|
||||
const int key_len = oakengine_ipc_shm_make_key(pid, 0, key, sizeof(key));
|
||||
(void)key_len;
|
||||
|
||||
// Attaching to a missing segment fails and reports an error.
|
||||
OakSharedMemoryRegion *missing = oakengine_ipc_shm_create();
|
||||
assert(oakengine_ipc_shm_open(missing, key, 4096,
|
||||
OAK_IPC_SHM_MODE_ATTACH) == 0);
|
||||
assert(oakengine_ipc_shm_is_valid(missing) == 0);
|
||||
char err[256];
|
||||
assert(oakengine_ipc_shm_error(missing, err, sizeof(err)) > 0);
|
||||
assert(strlen(err) > 0);
|
||||
oakengine_ipc_shm_free(missing);
|
||||
|
||||
// Create the owner: valid, sized, zero-initialized.
|
||||
OakSharedMemoryRegion *owner = oakengine_ipc_shm_create();
|
||||
assert(oakengine_ipc_shm_open(owner, key, 8192, OAK_IPC_SHM_MODE_CREATE) ==
|
||||
1);
|
||||
assert(oakengine_ipc_shm_is_valid(owner) == 1);
|
||||
assert(oakengine_ipc_shm_size(owner) == 8192);
|
||||
unsigned char *data = (unsigned char *)oakengine_ipc_shm_data(owner);
|
||||
assert(data != NULL);
|
||||
for (int i = 0; i < 8192; i++) {
|
||||
assert(data[i] == 0);
|
||||
}
|
||||
|
||||
// key() round-trips the opened key.
|
||||
char opened_key[OAK_IPC_SHM_KEY_CAP];
|
||||
assert(oakengine_ipc_shm_key(owner, opened_key, sizeof(opened_key)) ==
|
||||
key_len);
|
||||
assert(strcmp(opened_key, key) == 0);
|
||||
|
||||
// A peer attaches by the same key and sees the owner's bytes.
|
||||
data[0] = 0xAB;
|
||||
data[8191] = 0xCD;
|
||||
OakSharedMemoryRegion *peer = oakengine_ipc_shm_create();
|
||||
assert(oakengine_ipc_shm_open(peer, key, 8192, OAK_IPC_SHM_MODE_ATTACH) ==
|
||||
1);
|
||||
assert(oakengine_ipc_shm_size(peer) == 8192);
|
||||
const unsigned char *peer_data =
|
||||
(const unsigned char *)oakengine_ipc_shm_data(peer);
|
||||
assert(peer_data[0] == 0xAB && peer_data[8191] == 0xCD);
|
||||
peer_data = NULL;
|
||||
|
||||
// The peer closes without unlinking; the owner stays valid.
|
||||
oakengine_ipc_shm_close(peer);
|
||||
assert(oakengine_ipc_shm_is_valid(peer) == 0);
|
||||
assert(oakengine_ipc_shm_is_valid(owner) == 1);
|
||||
oakengine_ipc_shm_free(peer);
|
||||
|
||||
// The owner unlinks on close: a late attach must fail.
|
||||
oakengine_ipc_shm_close(owner);
|
||||
assert(oakengine_ipc_shm_is_valid(owner) == 0);
|
||||
OakSharedMemoryRegion *late = oakengine_ipc_shm_create();
|
||||
assert(oakengine_ipc_shm_open(late, key, 8192, OAK_IPC_SHM_MODE_ATTACH) ==
|
||||
0);
|
||||
oakengine_ipc_shm_free(late);
|
||||
oakengine_ipc_shm_free(owner);
|
||||
|
||||
// NULL safety.
|
||||
oakengine_ipc_shm_close(NULL);
|
||||
assert(oakengine_ipc_shm_is_valid(NULL) == 0);
|
||||
assert(oakengine_ipc_shm_data(NULL) == NULL);
|
||||
assert(oakengine_ipc_shm_size(NULL) == 0);
|
||||
oakengine_ipc_shm_free(NULL);
|
||||
}
|
||||
|
||||
static void test_framepool(void)
|
||||
{
|
||||
const uint32_t k_slots = 3;
|
||||
const size_t k_slot_bytes = 256;
|
||||
|
||||
// bytes_needed stays 64-aligned and grows with geometry.
|
||||
assert(oakengine_ipc_framepool_bytes_needed(k_slots, k_slot_bytes) % 64 ==
|
||||
0);
|
||||
assert(oakengine_ipc_framepool_bytes_needed(2, 65) ==
|
||||
oakengine_ipc_framepool_bytes_needed(2, 128));
|
||||
assert(oakengine_ipc_framepool_bytes_needed(1, 64) <
|
||||
oakengine_ipc_framepool_bytes_needed(2, 64));
|
||||
|
||||
const size_t bytes =
|
||||
oakengine_ipc_framepool_bytes_needed(k_slots, k_slot_bytes);
|
||||
void *mem = malloc(bytes);
|
||||
assert(mem != NULL);
|
||||
|
||||
OakFrameSlotPool *filler =
|
||||
oakengine_ipc_framepool_create(mem, k_slots, k_slot_bytes);
|
||||
assert(filler != NULL);
|
||||
assert(oakengine_ipc_framepool_is_valid(filler) == 1);
|
||||
assert(oakengine_ipc_framepool_slot_count(filler) == k_slots);
|
||||
assert(oakengine_ipc_framepool_slot_data_bytes(filler) == k_slot_bytes);
|
||||
|
||||
// Peer maps the same memory; a copy of the handle views it too.
|
||||
OakFrameSlotPool *drainer = oakengine_ipc_framepool_attach(mem);
|
||||
assert(oakengine_ipc_framepool_is_valid(drainer) == 1);
|
||||
OakFrameSlotPool *drainer_copy = oakengine_ipc_framepool_copy(drainer);
|
||||
assert(drainer_copy != NULL && drainer_copy != drainer);
|
||||
assert(oakengine_ipc_framepool_slot_count(drainer_copy) == k_slots);
|
||||
|
||||
// Free slots are issued FIFO; the pool then reports exhausted.
|
||||
uint32_t held[3];
|
||||
for (uint32_t i = 0; i < k_slots; i++) {
|
||||
assert(oakengine_ipc_framepool_acquire(filler, &held[i]) == 1);
|
||||
assert(held[i] == i);
|
||||
}
|
||||
uint32_t overflow = 99;
|
||||
assert(oakengine_ipc_framepool_acquire(filler, &overflow) == 0);
|
||||
assert(overflow == 99);
|
||||
|
||||
// Fill slot 0 with a pattern + full metadata and publish it.
|
||||
unsigned char *data =
|
||||
(unsigned char *)oakengine_ipc_framepool_slot_data(filler, held[0]);
|
||||
assert(data != NULL);
|
||||
for (size_t i = 0; i < k_slot_bytes; i++) {
|
||||
data[i] = (unsigned char)(i & 0xFF);
|
||||
}
|
||||
oak_frame_slot_meta *meta = oakengine_ipc_framepool_meta(filler, held[0]);
|
||||
assert(meta != NULL);
|
||||
meta->id = -4242;
|
||||
meta->time_num = 1001;
|
||||
meta->time_den = 30000;
|
||||
meta->width = 3840;
|
||||
meta->height = 2160;
|
||||
meta->format = 17;
|
||||
meta->channel_count = 4;
|
||||
meta->linesize = 3840 * 4 * 4;
|
||||
meta->data_size = (int32_t)k_slot_bytes;
|
||||
strncpy(meta->colorspace, "acescg", sizeof(meta->colorspace) - 1);
|
||||
meta->colorspace[sizeof(meta->colorspace) - 1] = '\0';
|
||||
assert(oakengine_ipc_framepool_publish(filler, held[0]) == 1);
|
||||
|
||||
// Publish slot 2 then slot 1: consume order follows publish order.
|
||||
assert(oakengine_ipc_framepool_publish(filler, held[2]) == 1);
|
||||
assert(oakengine_ipc_framepool_publish(filler, held[1]) == 1);
|
||||
|
||||
uint32_t got = 99;
|
||||
assert(oakengine_ipc_framepool_consume(drainer, &got) == 1);
|
||||
assert(got == held[0]);
|
||||
|
||||
// Metadata and pixels arrive intact on the drainer side.
|
||||
const oak_frame_slot_meta *out =
|
||||
oakengine_ipc_framepool_meta_const(drainer, got);
|
||||
assert(out != NULL);
|
||||
assert(out->id == -4242);
|
||||
assert(out->time_num == 1001 && out->time_den == 30000);
|
||||
assert(out->width == 3840 && out->height == 2160);
|
||||
assert(out->format == 17 && out->channel_count == 4);
|
||||
assert(out->linesize == 3840 * 4 * 4);
|
||||
assert(out->data_size == (int32_t)k_slot_bytes);
|
||||
assert(strcmp(out->colorspace, "acescg") == 0);
|
||||
const unsigned char *out_data =
|
||||
(const unsigned char *)oakengine_ipc_framepool_slot_data_const(drainer,
|
||||
got);
|
||||
assert(out_data != NULL);
|
||||
for (size_t i = 0; i < k_slot_bytes; i++) {
|
||||
assert(out_data[i] == (unsigned char)(i & 0xFF));
|
||||
}
|
||||
assert(oakengine_ipc_framepool_release(drainer, got) == 1);
|
||||
|
||||
// The copied handle consumes the remaining slots in publish order.
|
||||
assert(oakengine_ipc_framepool_consume(drainer_copy, &got) == 1);
|
||||
assert(got == held[2]);
|
||||
assert(oakengine_ipc_framepool_release(drainer_copy, got) == 1);
|
||||
assert(oakengine_ipc_framepool_consume(drainer_copy, &got) == 1);
|
||||
assert(got == held[1]);
|
||||
assert(oakengine_ipc_framepool_release(drainer_copy, got) == 1);
|
||||
assert(oakengine_ipc_framepool_consume(drainer, &got) == 0); // drained
|
||||
|
||||
// Released slots cycle back to the filler.
|
||||
uint32_t again = 0;
|
||||
assert(oakengine_ipc_framepool_acquire(filler, &again) == 1);
|
||||
|
||||
oakengine_ipc_framepool_free(drainer_copy);
|
||||
oakengine_ipc_framepool_free(drainer);
|
||||
oakengine_ipc_framepool_free(filler);
|
||||
free(mem);
|
||||
|
||||
// A region without the pool magic attaches as invalid.
|
||||
void *raw = calloc(1, oakengine_ipc_framepool_bytes_needed(2, 64));
|
||||
assert(raw != NULL);
|
||||
OakFrameSlotPool *bad = oakengine_ipc_framepool_attach(raw);
|
||||
assert(bad != NULL);
|
||||
assert(oakengine_ipc_framepool_is_valid(bad) == 0);
|
||||
assert(oakengine_ipc_framepool_slot_count(bad) == 0);
|
||||
assert(oakengine_ipc_framepool_slot_data_bytes(bad) == 0);
|
||||
oakengine_ipc_framepool_free(bad);
|
||||
free(raw);
|
||||
|
||||
// NULL safety.
|
||||
assert(oakengine_ipc_framepool_is_valid(NULL) == 0);
|
||||
assert(oakengine_ipc_framepool_slot_count(NULL) == 0);
|
||||
assert(oakengine_ipc_framepool_slot_data(NULL, 0) == NULL);
|
||||
assert(oakengine_ipc_framepool_meta(NULL, 0) == NULL);
|
||||
assert(oakengine_ipc_framepool_acquire(NULL, &again) == 0);
|
||||
oakengine_ipc_framepool_free(NULL);
|
||||
}
|
||||
|
||||
// Frame hand-off across two live shared-memory mappings, the app<->worker shape.
|
||||
static void test_framepool_over_shm(void)
|
||||
{
|
||||
char key[OAK_IPC_SHM_KEY_CAP];
|
||||
assert(oakengine_ipc_shm_make_key(getpid(), 7, key, sizeof(key)) > 0);
|
||||
|
||||
const uint32_t k_slots = 2;
|
||||
const size_t k_slot_bytes = 64;
|
||||
const size_t bytes =
|
||||
oakengine_ipc_framepool_bytes_needed(k_slots, k_slot_bytes);
|
||||
|
||||
OakSharedMemoryRegion *owner = oakengine_ipc_shm_create();
|
||||
assert(oakengine_ipc_shm_open(owner, key, bytes, OAK_IPC_SHM_MODE_CREATE) ==
|
||||
1);
|
||||
OakSharedMemoryRegion *peer = oakengine_ipc_shm_create();
|
||||
assert(oakengine_ipc_shm_open(peer, key, bytes, OAK_IPC_SHM_MODE_ATTACH) ==
|
||||
1);
|
||||
|
||||
OakFrameSlotPool *filler = oakengine_ipc_framepool_create(
|
||||
oakengine_ipc_shm_data(owner), k_slots, k_slot_bytes);
|
||||
OakFrameSlotPool *drainer = oakengine_ipc_framepool_attach(
|
||||
oakengine_ipc_shm_data(peer));
|
||||
assert(oakengine_ipc_framepool_is_valid(filler) == 1);
|
||||
assert(oakengine_ipc_framepool_is_valid(drainer) == 1);
|
||||
|
||||
uint32_t idx = 0;
|
||||
assert(oakengine_ipc_framepool_acquire(filler, &idx) == 1);
|
||||
oak_frame_slot_meta *meta = oakengine_ipc_framepool_meta(filler, idx);
|
||||
meta->id = 77;
|
||||
meta->data_size = (int32_t)k_slot_bytes;
|
||||
memset(oakengine_ipc_framepool_slot_data(filler, idx), 0x5A, k_slot_bytes);
|
||||
assert(oakengine_ipc_framepool_publish(filler, idx) == 1);
|
||||
|
||||
uint32_t got = 0;
|
||||
assert(oakengine_ipc_framepool_consume(drainer, &got) == 1);
|
||||
assert(got == idx);
|
||||
assert(oakengine_ipc_framepool_meta_const(drainer, got)->id == 77);
|
||||
const unsigned char *d = (const unsigned char *)
|
||||
oakengine_ipc_framepool_slot_data_const(drainer, got);
|
||||
assert(d[0] == 0x5A && d[k_slot_bytes - 1] == 0x5A);
|
||||
assert(oakengine_ipc_framepool_release(drainer, got) == 1);
|
||||
|
||||
oakengine_ipc_framepool_free(drainer);
|
||||
oakengine_ipc_framepool_free(filler);
|
||||
oakengine_ipc_shm_free(peer);
|
||||
oakengine_ipc_shm_free(owner); // owner frees last: unlinks the segment
|
||||
}
|
||||
|
||||
static void test_handshake(void)
|
||||
{
|
||||
oak_ipc_handshake hs;
|
||||
memset(&hs, 0, sizeof(hs));
|
||||
hs.protocol_version = 1;
|
||||
strcpy(hs.shm_key, "olive-rw-1234-0-out");
|
||||
strcpy(hs.input_shm_key, "olive-rw-1234-1-in");
|
||||
hs.input_slots = 4;
|
||||
hs.output_slots = 6;
|
||||
hs.slot_data_bytes = 256ll * 1024 * 1024;
|
||||
hs.input_slot_data_bytes = 128ll * 1024 * 1024;
|
||||
|
||||
const int needed = oakengine_ipc_handshake_to_json(&hs, NULL, 0);
|
||||
assert(needed > 0);
|
||||
char *json = (char *)malloc(size_t(needed) + 1);
|
||||
assert(json != NULL);
|
||||
assert(oakengine_ipc_handshake_to_json(&hs, json, needed + 1) == needed);
|
||||
assert((int)strlen(json) == needed);
|
||||
|
||||
// buf/size convention: a short buffer truncates but reports the full size.
|
||||
char tiny[8];
|
||||
assert(oakengine_ipc_handshake_to_json(&hs, tiny, sizeof(tiny)) == needed);
|
||||
assert(strlen(tiny) == sizeof(tiny) - 1);
|
||||
|
||||
assert(oakengine_ipc_message_type(json) == OAK_IPC_MSGTYPE_HANDSHAKE);
|
||||
|
||||
oak_ipc_handshake back;
|
||||
assert(oakengine_ipc_handshake_parse(json, &back) == 1);
|
||||
assert(back.protocol_version == hs.protocol_version);
|
||||
assert(strcmp(back.shm_key, hs.shm_key) == 0);
|
||||
assert(strcmp(back.input_shm_key, hs.input_shm_key) == 0);
|
||||
assert(back.input_slots == hs.input_slots);
|
||||
assert(back.output_slots == hs.output_slots);
|
||||
assert(back.slot_data_bytes == hs.slot_data_bytes);
|
||||
assert(back.input_slot_data_bytes == hs.input_slot_data_bytes);
|
||||
free(json);
|
||||
|
||||
// Exact wire text: same field names and compact shape the Qt builder emits.
|
||||
oak_ipc_handshake small;
|
||||
memset(&small, 0, sizeof(small));
|
||||
small.protocol_version = 1;
|
||||
strcpy(small.shm_key, "out");
|
||||
strcpy(small.input_shm_key, "in");
|
||||
small.input_slots = 4;
|
||||
small.output_slots = 6;
|
||||
small.slot_data_bytes = 256;
|
||||
small.input_slot_data_bytes = 128;
|
||||
const int n2 = oakengine_ipc_handshake_to_json(&small, NULL, 0);
|
||||
char *json2 = (char *)malloc(size_t(n2) + 1);
|
||||
assert(json2 != NULL);
|
||||
assert(oakengine_ipc_handshake_to_json(&small, json2, n2 + 1) == n2);
|
||||
assert_json_eq(json2,
|
||||
"{\"input_shm_key\":\"in\",\"input_slot_data_bytes\":128,"
|
||||
"\"input_slots\":4,\"output_slots\":6,\"protocol_version\":1,"
|
||||
"\"shm_key\":\"out\",\"slot_data_bytes\":256,"
|
||||
"\"type\":\"handshake\"}");
|
||||
free(json2);
|
||||
|
||||
assert(oakengine_ipc_handshake_to_json(NULL, NULL, 0) == -1);
|
||||
assert(oakengine_ipc_handshake_parse("not json", &back) == 0);
|
||||
assert(oakengine_ipc_handshake_parse(NULL, &back) == 0);
|
||||
}
|
||||
|
||||
static void test_render_frame(void)
|
||||
{
|
||||
oak_ipc_render_frame rf;
|
||||
memset(&rf, 0, sizeof(rf));
|
||||
rf.ticket_id = (int64_t(1) << 52) + 12345; // exact as a JSON double
|
||||
strcpy(rf.node_uuid, "{abcd-1234}");
|
||||
rf.time_num = (int64_t)48000 * 123456789;
|
||||
rf.time_den = int64_t(1) << 40;
|
||||
rf.width = 1920;
|
||||
rf.height = 1080;
|
||||
rf.format = 3;
|
||||
rf.channel_count = 4;
|
||||
rf.mode = 1;
|
||||
rf.input_slot = 2;
|
||||
rf.input_slots[0] = 2;
|
||||
rf.input_slots[1] = 3;
|
||||
rf.input_slot_count = 2;
|
||||
rf.has_color_transform = 1;
|
||||
rf.color_is_display = 1;
|
||||
strcpy(rf.color_output, "sRGB - Display");
|
||||
strcpy(rf.color_view, "ACES 1.0 SDR-video");
|
||||
strcpy(rf.color_look, "None");
|
||||
|
||||
const int needed = oakengine_ipc_render_frame_to_json(&rf, NULL, 0);
|
||||
char *json = (char *)malloc(size_t(needed) + 1);
|
||||
assert(json != NULL);
|
||||
assert(oakengine_ipc_render_frame_to_json(&rf, json, needed + 1) == needed);
|
||||
assert(oakengine_ipc_message_type(json) == OAK_IPC_MSGTYPE_RENDER_FRAME);
|
||||
|
||||
oak_ipc_render_frame back;
|
||||
assert(oakengine_ipc_render_frame_parse(json, &back) == 1);
|
||||
assert(back.ticket_id == rf.ticket_id);
|
||||
assert(strcmp(back.node_uuid, rf.node_uuid) == 0);
|
||||
assert(back.time_num == rf.time_num);
|
||||
assert(back.time_den == rf.time_den);
|
||||
assert(back.width == rf.width && back.height == rf.height);
|
||||
assert(back.format == rf.format && back.channel_count == rf.channel_count);
|
||||
assert(back.mode == rf.mode);
|
||||
assert(back.input_slot == rf.input_slot);
|
||||
assert(back.input_slot_count == 2);
|
||||
assert(back.input_slots[0] == 2 && back.input_slots[1] == 3);
|
||||
assert(back.has_color_transform == 1 && back.color_is_display == 1);
|
||||
assert(strcmp(back.color_output, rf.color_output) == 0);
|
||||
assert(strcmp(back.color_view, rf.color_view) == 0);
|
||||
assert(strcmp(back.color_look, rf.color_look) == 0);
|
||||
free(json);
|
||||
|
||||
// Without a color transform the color keys are omitted from the wire text.
|
||||
oak_ipc_render_frame plain;
|
||||
memset(&plain, 0, sizeof(plain));
|
||||
plain.ticket_id = 99;
|
||||
strcpy(plain.node_uuid, "{abcd-1234}");
|
||||
plain.time_num = 1001;
|
||||
plain.time_den = 30000;
|
||||
plain.width = 1920;
|
||||
plain.height = 1080;
|
||||
plain.format = 3;
|
||||
plain.channel_count = 4;
|
||||
plain.mode = 1;
|
||||
plain.input_slot = 2;
|
||||
plain.input_slots[0] = 2;
|
||||
plain.input_slots[1] = 3;
|
||||
plain.input_slot_count = 2;
|
||||
const int n2 = oakengine_ipc_render_frame_to_json(&plain, NULL, 0);
|
||||
char *json2 = (char *)malloc(size_t(n2) + 1);
|
||||
assert(json2 != NULL);
|
||||
assert(oakengine_ipc_render_frame_to_json(&plain, json2, n2 + 1) == n2);
|
||||
assert(strstr(json2, "has_color_transform") == NULL);
|
||||
assert_json_eq(json2,
|
||||
"{\"channels\":4,\"format\":3,\"height\":1080,"
|
||||
"\"input_slot\":2,\"input_slots\":[2,3],\"mode\":1,"
|
||||
"\"node\":\"{abcd-1234}\",\"ticket\":99,\"time_den\":30000,"
|
||||
"\"time_num\":1001,\"type\":\"render_frame\",\"width\":1920}");
|
||||
free(json2);
|
||||
|
||||
// Defaults for a sparse message, mirroring the Qt from_json defaults.
|
||||
oak_ipc_render_frame sparse;
|
||||
assert(oakengine_ipc_render_frame_parse("{\"type\":\"render_frame\"}",
|
||||
&sparse) == 1);
|
||||
assert(sparse.ticket_id == 0);
|
||||
assert(sparse.time_num == 0 && sparse.time_den == 1);
|
||||
assert(sparse.width == 0 && sparse.height == 0);
|
||||
assert(sparse.format == -1 && sparse.channel_count == 0 && sparse.mode == 0);
|
||||
assert(sparse.input_slot == -1 && sparse.input_slot_count == 0);
|
||||
assert(sparse.has_color_transform == 0);
|
||||
|
||||
// Legacy scalar input_slot folds into the array when it is absent.
|
||||
oak_ipc_render_frame legacy;
|
||||
assert(oakengine_ipc_render_frame_parse(
|
||||
"{\"type\":\"render_frame\",\"ticket\":5,\"input_slot\":3}",
|
||||
&legacy) == 1);
|
||||
assert(legacy.input_slot == 3);
|
||||
assert(legacy.input_slot_count == 1 && legacy.input_slots[0] == 3);
|
||||
|
||||
// Type mismatches are rejected.
|
||||
oak_ipc_handshake hs;
|
||||
memset(&hs, 0, sizeof(hs));
|
||||
const int hn = oakengine_ipc_handshake_to_json(&hs, NULL, 0);
|
||||
char *hjson = (char *)malloc(size_t(hn) + 1);
|
||||
assert(hjson != NULL);
|
||||
assert(oakengine_ipc_handshake_to_json(&hs, hjson, hn + 1) == hn);
|
||||
assert(oakengine_ipc_render_frame_parse(hjson, &back) == 0);
|
||||
assert(oakengine_ipc_handshake_parse(hjson, &hs) == 1);
|
||||
free(hjson);
|
||||
}
|
||||
|
||||
static void test_small_messages(void)
|
||||
{
|
||||
// frame_ready
|
||||
oak_ipc_frame_ready fr;
|
||||
fr.ticket_id = 99;
|
||||
fr.output_slot = 2;
|
||||
const int fn = oakengine_ipc_frame_ready_to_json(&fr, NULL, 0);
|
||||
char *fjson = (char *)malloc(size_t(fn) + 1);
|
||||
assert(fjson != NULL);
|
||||
assert(oakengine_ipc_frame_ready_to_json(&fr, fjson, fn + 1) == fn);
|
||||
assert_json_eq(fjson, "{\"slot\":2,\"ticket\":99,\"type\":\"frame_ready\"}");
|
||||
assert(oakengine_ipc_message_type(fjson) == OAK_IPC_MSGTYPE_FRAME_READY);
|
||||
oak_ipc_frame_ready fr_back;
|
||||
assert(oakengine_ipc_frame_ready_parse(fjson, &fr_back) == 1);
|
||||
assert(fr_back.ticket_id == 99 && fr_back.output_slot == 2);
|
||||
free(fjson);
|
||||
|
||||
// cancel
|
||||
oak_ipc_cancel cancel;
|
||||
cancel.ticket_id = 7;
|
||||
const int cn = oakengine_ipc_cancel_to_json(&cancel, NULL, 0);
|
||||
char *cjson = (char *)malloc(size_t(cn) + 1);
|
||||
assert(cjson != NULL);
|
||||
assert(oakengine_ipc_cancel_to_json(&cancel, cjson, cn + 1) == cn);
|
||||
assert_json_eq(cjson, "{\"ticket\":7,\"type\":\"cancel\"}");
|
||||
assert(oakengine_ipc_message_type(cjson) == OAK_IPC_MSGTYPE_CANCEL);
|
||||
oak_ipc_cancel cancel_back;
|
||||
assert(oakengine_ipc_cancel_parse(cjson, &cancel_back) == 1);
|
||||
assert(cancel_back.ticket_id == 7);
|
||||
// cancel rejects a frame_ready payload.
|
||||
assert(oakengine_ipc_cancel_parse("{\"slot\":2,\"ticket\":7,\"type\":"
|
||||
"\"frame_ready\"}",
|
||||
&cancel_back) == 0);
|
||||
free(cjson);
|
||||
|
||||
// load_graph
|
||||
oak_ipc_load_graph load;
|
||||
strcpy(load.path, "/tmp/oak-render-graph-abc123.ove");
|
||||
const int ln = oakengine_ipc_load_graph_to_json(&load, NULL, 0);
|
||||
char *ljson = (char *)malloc(size_t(ln) + 1);
|
||||
assert(ljson != NULL);
|
||||
assert(oakengine_ipc_load_graph_to_json(&load, ljson, ln + 1) == ln);
|
||||
assert_json_eq(ljson,
|
||||
"{\"path\":\"/tmp/oak-render-graph-abc123.ove\",\"type\":"
|
||||
"\"load_graph\"}");
|
||||
assert(oakengine_ipc_message_type(ljson) == OAK_IPC_MSGTYPE_LOAD_GRAPH);
|
||||
oak_ipc_load_graph load_back;
|
||||
assert(oakengine_ipc_load_graph_parse(ljson, &load_back) == 1);
|
||||
assert(strcmp(load_back.path, load.path) == 0);
|
||||
free(ljson);
|
||||
|
||||
// shutdown
|
||||
const int sn = oakengine_ipc_shutdown_to_json(NULL, 0);
|
||||
char *sjson = (char *)malloc(size_t(sn) + 1);
|
||||
assert(sjson != NULL);
|
||||
assert(oakengine_ipc_shutdown_to_json(sjson, sn + 1) == sn);
|
||||
assert_json_eq(sjson, "{\"type\":\"shutdown\"}");
|
||||
assert(oakengine_ipc_shutdown_parse(sjson) == 1);
|
||||
assert(oakengine_ipc_shutdown_parse("{\"type\":\"cancel\"}") == 0);
|
||||
assert(oakengine_ipc_message_type(sjson) == OAK_IPC_MSGTYPE_SHUTDOWN);
|
||||
free(sjson);
|
||||
|
||||
// error
|
||||
const int en = oakengine_ipc_error_to_json("boom", NULL, 0);
|
||||
char *ejson = (char *)malloc(size_t(en) + 1);
|
||||
assert(ejson != NULL);
|
||||
assert(oakengine_ipc_error_to_json("boom", ejson, en + 1) == en);
|
||||
assert_json_eq(ejson, "{\"message\":\"boom\",\"type\":\"error\"}");
|
||||
assert(oakengine_ipc_message_type(ejson) == OAK_IPC_MSGTYPE_ERROR);
|
||||
char msg[OAK_IPC_ERROR_MESSAGE_CAP];
|
||||
assert(oakengine_ipc_error_parse(ejson, msg, sizeof(msg)) == 1);
|
||||
assert(strcmp(msg, "boom") == 0);
|
||||
assert(oakengine_ipc_error_parse("{\"type\":\"shutdown\"}", msg,
|
||||
sizeof(msg)) == 0);
|
||||
free(ejson);
|
||||
|
||||
// Unknown and malformed input.
|
||||
assert(oakengine_ipc_message_type("{\"type\":\"nope\"}") ==
|
||||
OAK_IPC_MSGTYPE_UNKNOWN);
|
||||
assert(oakengine_ipc_message_type("garbage") == OAK_IPC_MSGTYPE_UNKNOWN);
|
||||
assert(oakengine_ipc_message_type(NULL) == OAK_IPC_MSGTYPE_UNKNOWN);
|
||||
assert(OAK_IPC_MSGTYPE_GRAPH_UPDATE != OAK_IPC_MSGTYPE_UNKNOWN);
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
test_shm();
|
||||
test_framepool();
|
||||
test_framepool_over_shm();
|
||||
test_handshake();
|
||||
test_render_frame();
|
||||
test_small_messages();
|
||||
|
||||
printf("oakengine_ipc_test: all assertions passed\n");
|
||||
return 0;
|
||||
}
|
||||
@@ -22,9 +22,9 @@
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "oakengine/spscringbuffer.h"
|
||||
#include "render/ipc/frameslotpool.h"
|
||||
#include "render/ipc/ipcmessage.h"
|
||||
#include "render/ipc/spscringbuffer.h"
|
||||
|
||||
using namespace olive::ipc;
|
||||
|
||||
|
||||
@@ -515,8 +515,12 @@ private:
|
||||
static_cast<void *>(nullptr)));
|
||||
ticket->setProperty(
|
||||
"ipc_input_pool",
|
||||
// The engine reads this back as the internal implementation object
|
||||
// (olive::engine::internal::ipc::FrameSlotPool), which is exactly
|
||||
// what the C handle points at.
|
||||
olive::QtUtils::ptr_to_value(input_pool_ ?
|
||||
static_cast<void *>(&*input_pool_) :
|
||||
static_cast<void *>(
|
||||
input_pool_->handle()) :
|
||||
static_cast<void *>(nullptr)));
|
||||
QVariantList input_slot_values;
|
||||
for (int slot : input_slots) {
|
||||
|
||||
Reference in New Issue
Block a user