Add render worker IPC loop

This commit is contained in:
2026-06-04 13:57:36 +08:00
parent 8c1c7658d9
commit b23e01b916
32 changed files with 2346 additions and 5651 deletions
+22
View File
@@ -95,6 +95,19 @@ add_executable(olive-editor
)
target_include_directories(olive-editor PUBLIC pluginSupport)
target_link_libraries(olive-editor PUBLIC OfxHost)
# Add render worker process (olive-render-worker).
# Reuses the libolive-editor object library so the worker shares the exact same render/node/codec
# code as the editor. It is a headless app that owns its own offscreen GL context. The link set is
# currently the full OLIVE_LIBRARIES for simplicity; trimming UI-only dependencies is a later-phase
# cleanup (see render-process-isolation plan).
add_executable(olive-render-worker
render/worker/workermain.cpp
$<TARGET_OBJECTS:libolive-editor>
$<TARGET_OBJECTS:olive-version-obj>
)
target_include_directories(olive-render-worker PUBLIC pluginSupport)
target_link_libraries(olive-render-worker PUBLIC OfxHost)
# Create docs if doxygen was found
if(DOXYGEN_FOUND)
set(DOXYGEN_PROJECT_NAME "Oak Video Editor")
@@ -139,18 +152,27 @@ endif()
# Set link libraries
target_link_libraries(olive-editor PRIVATE ${OLIVE_LIBRARIES})
target_link_libraries(libolive-editor PRIVATE ${OLIVE_LIBRARIES})
target_link_libraries(olive-render-worker PRIVATE ${OLIVE_LIBRARIES})
# Set compile options
target_compile_options(olive-editor PRIVATE ${OLIVE_COMPILE_OPTIONS})
target_compile_options(libolive-editor PRIVATE ${OLIVE_COMPILE_OPTIONS})
target_compile_options(olive-render-worker PRIVATE ${OLIVE_COMPILE_OPTIONS})
# Set global definitions
target_compile_definitions(olive-editor PRIVATE ${OLIVE_DEFINITIONS})
target_compile_definitions(libolive-editor PRIVATE ${OLIVE_DEFINITIONS})
target_compile_definitions(olive-render-worker PRIVATE ${OLIVE_DEFINITIONS})
# Set include dirs
target_include_directories(olive-editor PRIVATE ${OLIVE_INCLUDE_DIRS})
target_include_directories(libolive-editor PRIVATE ${OLIVE_INCLUDE_DIRS})
target_include_directories(olive-render-worker PRIVATE ${OLIVE_INCLUDE_DIRS})
# Install the render worker alongside the editor on Linux.
if (UNIX AND NOT APPLE)
install(TARGETS olive-render-worker RUNTIME DESTINATION bin)
endif()
# Add crash handler
if (GoogleCrashpad_FOUND AND Qt${QT_VERSION_MAJOR}Network_FOUND)
+4
View File
@@ -83,6 +83,10 @@ public:
QVector<Node *> nodes;
QHash<quintptr, Node *> node_ptrs;
QHash<Node *, QUuid> node_uuids;
Node::OutputConnections promised_connections;
};
@@ -199,6 +199,8 @@ ProjectSerializer211228::Load(Project *project, QXmlStreamReader *reader,
PostConnect(xml_node_data);
LoadData load_data;
load_data.node_ptrs = xml_node_data.node_ptrs;
load_data.node_uuids = xml_node_data.node_uuids;
// Resolve serialized properties (if any)
for (auto it = properties.cbegin(); it != properties.cend(); it++) {
@@ -383,6 +383,9 @@ ProjectSerializer220403::Load(Project *project, QXmlStreamReader *reader,
// Make connections
PostConnect(xml_node_data);
load_data.node_ptrs = xml_node_data.node_ptrs;
load_data.node_uuids = xml_node_data.node_uuids;
// Resolve serialized properties (if any)
for (auto it = properties.cbegin(); it != properties.cend(); it++) {
Node *node = xml_node_data.node_ptrs.value(it.key());
@@ -47,6 +47,7 @@ ProjectSerializer230220::Load(Project *project, QXmlStreamReader *reader,
while (XMLReadNextStartElement(reader)) {
if (reader->name() == QStringLiteral("project")) {
project_data = project->Load(reader);
load_data.node_ptrs = project_data.node_ptrs;
} else if (reader->name() == QStringLiteral("layout")) {
load_data.layout = MainWindowLayoutInfo::fromXml(
reader, project_data.node_ptrs);
@@ -294,6 +295,8 @@ ProjectSerializer230220::Load(Project *project, QXmlStreamReader *reader,
}
}
}
load_data.node_ptrs = project_data.node_ptrs;
} else if (reader->name() == QStringLiteral("properties")) {
while (XMLReadNextStartElement(reader)) {
if (reader->name() == QStringLiteral("node")) {
+1
View File
@@ -16,6 +16,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
add_subdirectory(job)
add_subdirectory(ipc)
add_subdirectory(ocioconf)
add_subdirectory(opengl)
add_subdirectory(plugin)
+27
View File
@@ -0,0 +1,27 @@
# 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/>.
set(OLIVE_SOURCES
${OLIVE_SOURCES}
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
)
+173
View File
@@ -0,0 +1,173 @@
/***
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 "frameslotpool.h"
#include <cstring>
namespace olive
{
namespace ipc
{
namespace
{
// Round `value` up to the next multiple of `align` (align must be a power of two).
size_t AlignUp(size_t value, size_t align)
{
return (value + (align - 1)) & ~(align - 1);
}
constexpr size_t kAlign = 64; // Cache-line alignment for each sub-region.
} // namespace
size_t FrameSlotPool::BytesNeeded(uint32_t slot_count, size_t slot_data_bytes)
{
const uint32_t ring_cap = RingCapacity(slot_count);
size_t total = AlignUp(sizeof(Header), kAlign);
total += AlignUp(SpscRingBuffer::BytesNeeded(ring_cap), kAlign); // free ring
total += AlignUp(SpscRingBuffer::BytesNeeded(ring_cap), kAlign); // ready ring
total += AlignUp(sizeof(FrameSlotMeta) * slot_count, kAlign); // metadata array
total += AlignUp(slot_data_bytes, kAlign) * slot_count; // pixel data blocks
return total;
}
FrameSlotPool FrameSlotPool::Create(void *mem, uint32_t slot_count,
size_t slot_data_bytes)
{
FrameSlotPool pool;
pool.base_ = reinterpret_cast<uint8_t *>(mem);
const uint32_t ring_cap = RingCapacity(slot_count);
size_t offset = 0;
const size_t header_off = offset;
offset += AlignUp(sizeof(Header), kAlign);
const size_t free_off = offset;
offset += AlignUp(SpscRingBuffer::BytesNeeded(ring_cap), kAlign);
const size_t ready_off = offset;
offset += AlignUp(SpscRingBuffer::BytesNeeded(ring_cap), kAlign);
const size_t meta_off = offset;
offset += AlignUp(sizeof(FrameSlotMeta) * slot_count, kAlign);
const size_t data_off = offset;
pool.header_ = reinterpret_cast<Header *>(pool.base_ + header_off);
pool.header_->magic = kMagic;
pool.header_->slot_count = slot_count;
pool.header_->slot_data_bytes = slot_data_bytes;
pool.header_->free_ring_offset = free_off;
pool.header_->ready_ring_offset = ready_off;
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.meta_ = reinterpret_cast<FrameSlotMeta *>(pool.base_ + meta_off);
pool.data_ = pool.base_ + data_off;
memset(pool.meta_, 0, sizeof(FrameSlotMeta) * slot_count);
// Seed the free ring with every slot index so the filler can Acquire() immediately.
for (uint32_t i = 0; i < slot_count; i++) {
pool.free_ring_->Push(i);
}
return pool;
}
FrameSlotPool FrameSlotPool::Attach(void *mem)
{
FrameSlotPool pool;
pool.base_ = reinterpret_cast<uint8_t *>(mem);
pool.header_ = reinterpret_cast<Header *>(pool.base_);
if (pool.header_->magic != kMagic) {
// Caller will see IsValid() == false via a null header reset.
pool.header_ = nullptr;
pool.base_ = nullptr;
return pool;
}
pool.free_ring_ = SpscRingBuffer::Attach(pool.base_ + pool.header_->free_ring_offset);
pool.ready_ring_ = 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;
return pool;
}
uint32_t FrameSlotPool::slot_count() const
{
return header_ ? header_->slot_count : 0;
}
size_t FrameSlotPool::slot_data_bytes() const
{
return header_ ? size_t(header_->slot_data_bytes) : 0;
}
bool FrameSlotPool::Acquire(uint32_t *index)
{
return free_ring_->Pop(index);
}
void *FrameSlotPool::SlotData(uint32_t index)
{
return data_ + size_t(index) * AlignUp(slot_data_bytes(), kAlign);
}
const void *FrameSlotPool::SlotData(uint32_t index) const
{
return data_ + size_t(index) * AlignUp(slot_data_bytes(), kAlign);
}
FrameSlotMeta *FrameSlotPool::Meta(uint32_t index)
{
return &meta_[index];
}
const FrameSlotMeta *FrameSlotPool::Meta(uint32_t index) const
{
return &meta_[index];
}
bool FrameSlotPool::Publish(uint32_t index)
{
return ready_ring_->Push(index);
}
bool FrameSlotPool::Consume(uint32_t *index)
{
return ready_ring_->Pop(index);
}
bool FrameSlotPool::Release(uint32_t index)
{
return free_ring_->Push(index);
}
} // namespace ipc
} // namespace olive
+180
View File
@@ -0,0 +1,180 @@
/***
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 IPC_FRAMESLOTPOOL_H
#define IPC_FRAMESLOTPOOL_H
#include <cstddef>
#include <cstdint>
#include "spscringbuffer.h"
namespace olive
{
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).
*/
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.
};
/**
* @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 BytesNeeded(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 IsValid() 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 *SlotData(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 *SlotData(uint32_t index) const;
private:
FrameSlotPool() = default;
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 kMagic = 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 RingCapacity(uint32_t slot_count)
{
return slot_count + 1;
}
uint8_t *base_ = nullptr;
Header *header_ = nullptr;
SpscRingBuffer *free_ring_ = nullptr;
SpscRingBuffer *ready_ring_ = nullptr;
FrameSlotMeta *meta_ = nullptr;
uint8_t *data_ = nullptr;
};
} // namespace ipc
} // namespace olive
#endif // IPC_FRAMESLOTPOOL_H
+195
View File
@@ -0,0 +1,195 @@
/***
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 "ipcmessage.h"
#include <QJsonDocument>
#include <QIODevice>
namespace olive
{
namespace ipc
{
bool WriteMessage(QIODevice *device, const QJsonObject &obj)
{
QByteArray line = QJsonDocument(obj).toJson(QJsonDocument::Compact);
line.append('\n');
return device->write(line) == line.size();
}
bool ReadMessage(QByteArray *buffer, QJsonObject *out, bool *ok)
{
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()) {
if (ok) {
*ok = false;
}
return false;
}
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;
}
// ---- HandshakeMsg ---------------------------------------------------------------------------
QJsonObject HandshakeMsg::ToJson() const
{
QJsonObject o;
o["type"] = msgtype::kHandshake;
o["protocol_version"] = protocol_version;
o["shm_key"] = shm_key;
o["input_slots"] = input_slots;
o["output_slots"] = output_slots;
o["slot_data_bytes"] = double(slot_data_bytes);
return o;
}
bool HandshakeMsg::FromJson(const QJsonObject &o, HandshakeMsg *out)
{
if (o["type"].toString() != QLatin1String(msgtype::kHandshake)) {
return false;
}
out->protocol_version = o["protocol_version"].toInt();
out->shm_key = o["shm_key"].toString();
out->input_slots = o["input_slots"].toInt();
out->output_slots = o["output_slots"].toInt();
out->slot_data_bytes = qint64(o["slot_data_bytes"].toDouble());
return true;
}
// ---- RenderFrameMsg -------------------------------------------------------------------------
QJsonObject RenderFrameMsg::ToJson() const
{
QJsonObject o;
o["type"] = msgtype::kRenderFrame;
o["ticket"] = double(ticket_id);
o["node"] = node_uuid;
o["time_num"] = double(time_num);
o["time_den"] = double(time_den);
o["width"] = width;
o["height"] = height;
o["format"] = format;
o["channels"] = channel_count;
o["mode"] = mode;
return o;
}
bool RenderFrameMsg::FromJson(const QJsonObject &o, RenderFrameMsg *out)
{
if (o["type"].toString() != QLatin1String(msgtype::kRenderFrame)) {
return false;
}
out->ticket_id = qint64(o["ticket"].toDouble());
out->node_uuid = o["node"].toString();
out->time_num = qint64(o["time_num"].toDouble());
out->time_den = qint64(o["time_den"].toDouble(1));
out->width = o["width"].toInt();
out->height = o["height"].toInt();
out->format = o["format"].toInt(-1);
out->channel_count = o["channels"].toInt();
out->mode = o["mode"].toInt();
return true;
}
// ---- FrameReadyMsg --------------------------------------------------------------------------
QJsonObject FrameReadyMsg::ToJson() const
{
QJsonObject o;
o["type"] = msgtype::kFrameReady;
o["ticket"] = double(ticket_id);
o["slot"] = output_slot;
return o;
}
bool FrameReadyMsg::FromJson(const QJsonObject &o, FrameReadyMsg *out)
{
if (o["type"].toString() != QLatin1String(msgtype::kFrameReady)) {
return false;
}
out->ticket_id = qint64(o["ticket"].toDouble());
out->output_slot = o["slot"].toInt();
return true;
}
// ---- CancelMsg ------------------------------------------------------------------------------
QJsonObject CancelMsg::ToJson() const
{
QJsonObject o;
o["type"] = msgtype::kCancel;
o["ticket"] = double(ticket_id);
return o;
}
bool CancelMsg::FromJson(const QJsonObject &o, CancelMsg *out)
{
if (o["type"].toString() != QLatin1String(msgtype::kCancel)) {
return false;
}
out->ticket_id = qint64(o["ticket"].toDouble());
return true;
}
// ---- LoadGraphMsg ---------------------------------------------------------------------------
QJsonObject LoadGraphMsg::ToJson() const
{
QJsonObject o;
o["type"] = msgtype::kLoadGraph;
o["path"] = path;
return o;
}
bool LoadGraphMsg::FromJson(const QJsonObject &o, LoadGraphMsg *out)
{
if (o["type"].toString() != QLatin1String(msgtype::kLoadGraph)) {
return false;
}
out->path = o["path"].toString();
return true;
}
} // namespace ipc
} // namespace olive
+143
View File
@@ -0,0 +1,143 @@
/***
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 IPC_IPCMESSAGE_H
#define IPC_IPCMESSAGE_H
#include <cstdint>
#include <QByteArray>
#include <QJsonObject>
#include <QString>
class QIODevice;
namespace olive
{
namespace ipc
{
/**
* @brief Control-plane protocol exchanged over stdio between main and render worker.
*
* The wire format is NDJSON: one compact QJsonObject per line, terminated by '\n'. This is
* deliberately human-readable so the channel can be inspected live with `tee`/`cat` and test
* messages can be injected by hand. The stdio channel carries only low-frequency control traffic;
* bulk pixel data travels through the shared-memory FrameSlotPool, and the (potentially large)
* serialized node graph travels via a temporary file referenced by path.
*
* 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 *kHandshake = "handshake";
constexpr const char *kLoadGraph = "load_graph";
constexpr const char *kRenderFrame = "render_frame";
constexpr const char *kFrameReady = "frame_ready";
constexpr const char *kCancel = "cancel";
constexpr const char *kGraphUpdate = "graph_update";
constexpr const char *kShutdown = "shutdown";
constexpr const char *kError = "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 WriteMessage(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 ReadMessage(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; ///< Shared-memory segment key for this worker.
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-slot pixel block size (max frame size).
QJsonObject ToJson() const;
static bool FromJson(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.
QJsonObject ToJson() const;
static bool FromJson(const QJsonObject &o, RenderFrameMsg *out);
};
struct FrameReadyMsg {
qint64 ticket_id = 0;
int output_slot = 0; ///< Index into the worker->main output FrameSlotPool.
QJsonObject ToJson() const;
static bool FromJson(const QJsonObject &o, FrameReadyMsg *out);
};
struct CancelMsg {
qint64 ticket_id = 0;
QJsonObject ToJson() const;
static bool FromJson(const QJsonObject &o, CancelMsg *out);
};
struct LoadGraphMsg {
QString path; ///< Temporary file holding the serialized node graph.
QJsonObject ToJson() const;
static bool FromJson(const QJsonObject &o, LoadGraphMsg *out);
};
} // namespace ipc
} // namespace olive
#endif // IPC_IPCMESSAGE_H
+205
View File
@@ -0,0 +1,205 @@
/***
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 "sharedmemoryregion.h"
#include <QtGlobal>
#if defined(Q_OS_WIN)
#include <windows.h>
#else
#include <errno.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>
#include <cstring>
#endif
namespace olive
{
namespace ipc
{
SharedMemoryRegion::SharedMemoryRegion()
: size_(0)
, data_(nullptr)
, mode_(kAttach)
#if defined(Q_OS_WIN)
, handle_(nullptr)
#else
, fd_(-1)
#endif
{
}
SharedMemoryRegion::~SharedMemoryRegion()
{
Close();
}
QString SharedMemoryRegion::MakeKey(qint64 owner_pid, int worker_index)
{
return QStringLiteral("olive-rw-%1-%2").arg(owner_pid).arg(worker_index);
}
#if defined(Q_OS_WIN)
bool SharedMemoryRegion::Open(const QString &key, size_t size, Mode mode)
{
Close();
key_ = key;
size_ = size;
mode_ = mode;
// Windows global mapping names live in the Local\ namespace by default for the session.
const QString mapping_name = QStringLiteral("Local\\") + key;
const std::wstring wname = mapping_name.toStdWString();
if (mode == kCreate) {
const DWORD size_high = static_cast<DWORD>((quint64(size) >> 32) & 0xFFFFFFFF);
const DWORD size_low = static_cast<DWORD>(quint64(size) & 0xFFFFFFFF);
handle_ = CreateFileMappingW(INVALID_HANDLE_VALUE, nullptr, PAGE_READWRITE,
size_high, size_low, wname.c_str());
if (!handle_) {
error_ = QStringLiteral("CreateFileMapping failed: %1").arg(GetLastError());
return false;
}
if (GetLastError() == ERROR_ALREADY_EXISTS) {
error_ = QStringLiteral("Shared memory key already exists: %1").arg(key);
CloseHandle(handle_);
handle_ = nullptr;
return false;
}
} else {
handle_ = OpenFileMappingW(FILE_MAP_ALL_ACCESS, FALSE, wname.c_str());
if (!handle_) {
error_ = QStringLiteral("OpenFileMapping failed: %1").arg(GetLastError());
return false;
}
}
data_ = MapViewOfFile(handle_, FILE_MAP_ALL_ACCESS, 0, 0, size);
if (!data_) {
error_ = QStringLiteral("MapViewOfFile failed: %1").arg(GetLastError());
CloseHandle(handle_);
handle_ = nullptr;
return false;
}
if (mode == kCreate) {
memset(data_, 0, size);
}
return true;
}
void SharedMemoryRegion::Close()
{
if (data_) {
UnmapViewOfFile(data_);
data_ = nullptr;
}
if (handle_) {
CloseHandle(handle_);
handle_ = nullptr;
}
size_ = 0;
}
#else // POSIX
bool SharedMemoryRegion::Open(const QString &key, size_t size, Mode mode)
{
Close();
key_ = key;
size_ = size;
mode_ = mode;
// POSIX shared memory names must start with a single slash and contain no others.
shm_name_ = QStringLiteral("/") + QString(key).replace('/', '_');
const QByteArray name_bytes = shm_name_.toUtf8();
int oflag = O_RDWR;
if (mode == kCreate) {
oflag |= O_CREAT | O_EXCL;
// Clear any stale segment left by a crashed previous run with the same name.
shm_unlink(name_bytes.constData());
}
fd_ = shm_open(name_bytes.constData(), oflag, 0600);
if (fd_ < 0) {
error_ = QStringLiteral("shm_open(%1) failed: %2")
.arg(shm_name_, QString::fromUtf8(strerror(errno)));
return false;
}
if (mode == kCreate) {
if (ftruncate(fd_, off_t(size)) != 0) {
error_ = QStringLiteral("ftruncate failed: %1")
.arg(QString::fromUtf8(strerror(errno)));
::close(fd_);
fd_ = -1;
shm_unlink(name_bytes.constData());
return false;
}
}
data_ = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
if (data_ == MAP_FAILED) {
error_ = QStringLiteral("mmap failed: %1").arg(QString::fromUtf8(strerror(errno)));
data_ = nullptr;
::close(fd_);
fd_ = -1;
if (mode == kCreate) {
shm_unlink(name_bytes.constData());
}
return false;
}
if (mode == kCreate) {
memset(data_, 0, size);
}
return true;
}
void SharedMemoryRegion::Close()
{
if (data_) {
munmap(data_, size_);
data_ = nullptr;
}
if (fd_ >= 0) {
::close(fd_);
fd_ = -1;
}
if (mode_ == kCreate && !shm_name_.isEmpty()) {
// Only the owner unlinks, so the name is freed once both sides have unmapped.
shm_unlink(shm_name_.toUtf8().constData());
shm_name_.clear();
}
size_ = 0;
}
#endif
} // namespace ipc
} // namespace olive
+123
View File
@@ -0,0 +1,123 @@
/***
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 IPC_SHAREDMEMORYREGION_H
#define IPC_SHAREDMEMORYREGION_H
#include <cstddef>
#include <QString>
namespace olive
{
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.
kCreate,
/// Attach to a segment created by the peer. Does not unlink on destruction.
kAttach
};
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 IsValid() 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 MakeKey(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 olive
#endif // IPC_SHAREDMEMORYREGION_H
+184
View File
@@ -0,0 +1,184 @@
/***
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 IPC_SPSCRINGBUFFER_H
#define IPC_SPSCRINGBUFFER_H
#include <atomic>
#include <cstddef>
#include <cstdint>
namespace olive
{
namespace ipc
{
/**
* @brief A lock-free single-producer / single-consumer ring buffer of uint32 indices.
*
* This is the core synchronization primitive for cross-process communication. It is designed to
* live in a shared memory segment: the control block (head/tail cursors) and the slot array are a
* single trivially-copyable, self-contained POD. Both the producer process and the consumer
* process map the same memory and operate on it concurrently.
*
* Correctness relies on the classic SPSC invariant:
* - Exactly ONE thread/process calls Push() (the producer).
* - Exactly ONE thread/process calls Pop() (the consumer).
* Under that invariant, no mutex is required. The producer only ever writes `head_`, the consumer
* only ever writes `tail_`, and the acquire/release ordering on those two atomics publishes the
* slot writes safely across the process boundary.
*
* Capacity note: one slot is always left empty to disambiguate the full and empty states, so a
* buffer constructed with kCapacity slots can hold at most (kCapacity - 1) live entries.
*
* The payload stored is a single uint32_t per entry, intended to be an index into a separately
* managed slot pool (see FrameSlotPool). We never put pointers in shared memory.
*/
class SpscRingBuffer {
public:
/**
* @brief In-place construct a ring buffer header at `mem` with `capacity` index slots.
*
* `mem` must point to at least BytesNeeded(capacity) bytes of zero-initializable memory. This is
* intended to be placement-style initialization performed exactly once by whichever process owns
* the segment's creation; the peer process uses Attach() instead.
*/
static SpscRingBuffer *Create(void *mem, uint32_t capacity)
{
auto *self = reinterpret_cast<SpscRingBuffer *>(mem);
self->capacity_ = capacity;
self->head_.store(0, std::memory_order_relaxed);
self->tail_.store(0, std::memory_order_relaxed);
for (uint32_t i = 0; i < capacity; i++) {
self->slot_array()[i] = 0;
}
return self;
}
/**
* @brief Re-interpret already-initialized memory as a ring buffer (peer process side).
*
* No writes are performed; the cursors and capacity are assumed already set by Create().
*/
static SpscRingBuffer *Attach(void *mem)
{
return reinterpret_cast<SpscRingBuffer *>(mem);
}
/**
* @brief Total bytes required to hold the header plus `capacity` index slots.
*/
static size_t BytesNeeded(uint32_t capacity)
{
return sizeof(SpscRingBuffer) + size_t(capacity) * sizeof(uint32_t);
}
/**
* @brief Producer side: enqueue an index. Returns false if the buffer is full.
*/
bool Push(uint32_t value)
{
const uint32_t head = head_.load(std::memory_order_relaxed);
const uint32_t next = Increment(head);
// Buffer is full if advancing head would collide with the consumer's tail.
if (next == tail_.load(std::memory_order_acquire)) {
return false;
}
slot_array()[head] = value;
head_.store(next, std::memory_order_release);
return true;
}
/**
* @brief Consumer side: dequeue an index into `out`. Returns false if the buffer is empty.
*/
bool Pop(uint32_t *out)
{
const uint32_t tail = tail_.load(std::memory_order_relaxed);
// Buffer is empty if the consumer has caught up to the producer.
if (tail == head_.load(std::memory_order_acquire)) {
return false;
}
*out = slot_array()[tail];
tail_.store(Increment(tail), std::memory_order_release);
return true;
}
/**
* @brief Approximate number of entries currently queued.
*
* Safe to call from either side, but the value may be stale the instant it returns. Intended for
* metrics/backpressure heuristics, not for correctness decisions.
*/
uint32_t SizeApprox() const
{
const uint32_t head = head_.load(std::memory_order_acquire);
const uint32_t tail = tail_.load(std::memory_order_acquire);
return (head + capacity_ - tail) % capacity_;
}
bool IsEmptyApprox() const
{
return head_.load(std::memory_order_acquire) ==
tail_.load(std::memory_order_acquire);
}
uint32_t capacity() const
{
return capacity_;
}
private:
uint32_t Increment(uint32_t index) const
{
// capacity_ is small and this avoids requiring a power-of-two capacity.
return (index + 1) % capacity_;
}
// The index slot array is allocated immediately after this struct in the same contiguous block.
// (Named slot_array() rather than slots() to avoid Qt's `slots` keyword macro.)
uint32_t *slot_array()
{
return reinterpret_cast<uint32_t *>(this + 1);
}
const uint32_t *slot_array() const
{
return reinterpret_cast<const uint32_t *>(this + 1);
}
// Producer writes head_, consumer writes tail_. Kept on separate cache lines would be ideal, but
// since these live in shared memory with a trailing flexible array we keep the header compact and
// rely on acquire/release ordering for correctness.
std::atomic<uint32_t> head_;
std::atomic<uint32_t> tail_;
uint32_t capacity_;
static_assert(sizeof(std::atomic<uint32_t>) == sizeof(uint32_t),
"atomic<uint32_t> must be lock-free POD-sized for shared memory use");
};
} // namespace ipc
} // namespace olive
#endif // IPC_SPSCRINGBUFFER_H
+437
View File
@@ -0,0 +1,437 @@
/***
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 <cstdio>
#include <cstring>
#include <memory>
#include <optional>
#include <QFile>
#include <QGuiApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMatrix4x4>
#include <QOpenGLContext>
#include <QSurfaceFormat>
#include "common/qtutils.h"
#include "config/config.h"
#include "node/factory.h"
#include "node/input/multicam/multicamnode.h"
#include "node/project/serializer/serializer.h"
#include "render/ipc/frameslotpool.h"
#include "render/ipc/ipcmessage.h"
#include "render/ipc/sharedmemoryregion.h"
#include "render/opengl/openglrenderer.h"
#include "render/rendermanager.h"
#include "render/renderprocessor.h"
namespace
{
constexpr int kProtocolVersion = 1;
constexpr int kDefaultWidth = 1920;
constexpr int kDefaultHeight = 1080;
constexpr int kDefaultFrameRate = 24;
void InstallSurfaceFormat()
{
QSurfaceFormat format;
format.setVersion(3, 2);
format.setProfile(QSurfaceFormat::CoreProfile);
format.setDepthBufferSize(24);
QSurfaceFormat::setDefaultFormat(format);
}
void LogError(const QString &message)
{
const QByteArray line = QByteArray("worker: ") + message.toUtf8() + '\n';
fwrite(line.constData(), 1, size_t(line.size()), stderr);
fflush(stderr);
}
QJsonObject ErrorMessage(const QString &message, qint64 ticket_id = 0)
{
QJsonObject o;
o["type"] = olive::ipc::msgtype::kError;
o["message"] = message;
if (ticket_id) {
o["ticket"] = double(ticket_id);
}
return o;
}
class RenderWorker {
public:
RenderWorker(olive::OpenGLRenderer *renderer, QFile *out)
: renderer_(renderer)
, out_(out)
{
}
~RenderWorker()
{
project_.reset();
olive::ProjectSerializer::Destroy();
olive::NodeFactory::Destroy();
}
bool InitializeRuntime()
{
olive::Config::Load();
olive::NodeFactory::Initialize();
olive::ColorManager::SetUpDefaultConfig();
olive::ProjectSerializer::Initialize();
return true;
}
bool SendStartupHandshake()
{
olive::ipc::HandshakeMsg hs;
hs.protocol_version = kProtocolVersion;
hs.shm_key = QString();
hs.input_slots = 0;
hs.output_slots = 0;
hs.slot_data_bytes = 0;
QJsonObject handshake = hs.ToJson();
if (QOpenGLContext *ctx = renderer_->context()) {
const QSurfaceFormat fmt = ctx->format();
handshake["gl_major"] = fmt.majorVersion();
handshake["gl_minor"] = fmt.minorVersion();
}
return Write(handshake);
}
bool Handle(const QJsonObject &message)
{
const QString type = message["type"].toString();
if (type == QLatin1String(olive::ipc::msgtype::kHandshake)) {
olive::ipc::HandshakeMsg hs;
if (!olive::ipc::HandshakeMsg::FromJson(message, &hs)) {
return Write(ErrorMessage(QStringLiteral("invalid handshake message")));
}
return AttachOutputPool(hs);
}
if (type == QLatin1String(olive::ipc::msgtype::kLoadGraph)) {
olive::ipc::LoadGraphMsg load;
if (!olive::ipc::LoadGraphMsg::FromJson(message, &load)) {
return Write(ErrorMessage(QStringLiteral("invalid load_graph message")));
}
return LoadGraph(load.path);
}
if (type == QLatin1String(olive::ipc::msgtype::kRenderFrame)) {
olive::ipc::RenderFrameMsg render;
if (!olive::ipc::RenderFrameMsg::FromJson(message, &render)) {
return Write(ErrorMessage(QStringLiteral("invalid render_frame message")));
}
return RenderFrame(render);
}
if (type == QLatin1String(olive::ipc::msgtype::kCancel)) {
// Stage 5 wires cancellation into in-flight jobs. Stage 2 has only synchronous single-frame work.
return true;
}
if (type == QLatin1String(olive::ipc::msgtype::kShutdown)) {
shutdown_requested_ = true;
return true;
}
return Write(ErrorMessage(QStringLiteral("unknown message type: %1").arg(type)));
}
bool shutdown_requested() const
{
return shutdown_requested_;
}
private:
bool Write(const QJsonObject &message)
{
const bool ok = olive::ipc::WriteMessage(out_, message);
out_->flush();
return ok;
}
bool AttachOutputPool(const olive::ipc::HandshakeMsg &hs)
{
if (hs.protocol_version != kProtocolVersion) {
return Write(ErrorMessage(QStringLiteral("unsupported protocol version %1")
.arg(hs.protocol_version)));
}
if (hs.shm_key.isEmpty() || hs.output_slots <= 0 || hs.slot_data_bytes <= 0) {
return Write(ErrorMessage(QStringLiteral("handshake missing output shared-memory geometry")));
}
const size_t bytes = olive::ipc::FrameSlotPool::BytesNeeded(
uint32_t(hs.output_slots), size_t(hs.slot_data_bytes));
if (!output_region_.Open(hs.shm_key, bytes, olive::ipc::SharedMemoryRegion::kAttach)) {
return Write(ErrorMessage(QStringLiteral("failed to attach shared memory: %1")
.arg(output_region_.error())));
}
output_pool_ = olive::ipc::FrameSlotPool::Attach(output_region_.data());
if (!output_pool_->IsValid()) {
output_region_.Close();
output_pool_.reset();
return Write(ErrorMessage(QStringLiteral("shared memory does not contain a frame slot pool")));
}
return true;
}
bool LoadGraph(const QString &path)
{
auto loaded = std::make_unique<olive::Project>();
loaded->Initialize();
olive::ProjectSerializer::Result result =
olive::ProjectSerializer::Load(loaded.get(), path, olive::ProjectSerializer::kProject);
if (result != olive::ProjectSerializer::kSuccess) {
return Write(ErrorMessage(QStringLiteral("failed to load graph %1: %2")
.arg(path, result.GetDetails())));
}
project_ = std::move(loaded);
node_by_token_.clear();
const auto &data = result.GetLoadData();
for (auto it = data.node_ptrs.cbegin(); it != data.node_ptrs.cend(); ++it) {
node_by_token_.insert(QString::number(it.key()), it.value());
}
for (auto it = data.node_uuids.cbegin(); it != data.node_uuids.cend(); ++it) {
node_by_token_.insert(it.value().toString(), it.key());
node_by_token_.insert(it.value().toString(QUuid::WithoutBraces), it.key());
}
QJsonObject ack;
ack["type"] = QStringLiteral("graph_loaded");
ack["nodes"] = node_by_token_.size();
return Write(ack);
}
olive::Node *FindNode(const QString &token) const
{
if (olive::Node *node = node_by_token_.value(token, nullptr)) {
return node;
}
bool ok = false;
const quintptr ptr = token.toULongLong(&ok, 0);
if (ok) {
return node_by_token_.value(QString::number(ptr), nullptr);
}
return nullptr;
}
bool RenderFrame(const olive::ipc::RenderFrameMsg &message)
{
if (!project_) {
return Write(ErrorMessage(QStringLiteral("render_frame received before load_graph"),
message.ticket_id));
}
if (!output_pool_ || !output_pool_->IsValid()) {
return Write(ErrorMessage(QStringLiteral("render_frame received before output shm handshake"),
message.ticket_id));
}
olive::Node *node = FindNode(message.node_uuid);
if (!node) {
return Write(ErrorMessage(QStringLiteral("render node not found: %1").arg(message.node_uuid),
message.ticket_id));
}
olive::VideoParams vparams(message.width > 0 ? message.width : kDefaultWidth,
message.height > 0 ? message.height : kDefaultHeight,
olive::rational(1, kDefaultFrameRate),
message.format >= 0
? olive::PixelFormat::Format(message.format)
: olive::PixelFormat::F32,
message.channel_count > 0
? message.channel_count
: olive::VideoParams::kRGBAChannelCount);
olive::RenderTicketPtr ticket = std::make_shared<olive::RenderTicket>();
ticket->setProperty("node", olive::QtUtils::PtrToValue(node));
ticket->setProperty("time", QVariant::fromValue(
olive::rational(int(message.time_num), int(message.time_den))));
ticket->setProperty("size", QSize(message.width, message.height));
ticket->setProperty("matrix", QMatrix4x4());
ticket->setProperty("format",
message.format >= 0
? olive::PixelFormat::Format(message.format)
: olive::PixelFormat::INVALID);
ticket->setProperty("usecache", false);
ticket->setProperty("channelcount", message.channel_count);
ticket->setProperty("mode", olive::RenderMode::Mode(message.mode));
ticket->setProperty("type", olive::RenderManager::kTypeVideo);
ticket->setProperty("colormanager", olive::QtUtils::PtrToValue(project_->color_manager()));
ticket->setProperty("coloroutput", QVariant::fromValue(olive::ColorProcessorPtr()));
ticket->setProperty("vparam", QVariant::fromValue(vparams));
ticket->setProperty("aparam", QVariant::fromValue(olive::AudioParams()));
ticket->setProperty("return", olive::RenderManager::kFrame);
ticket->setProperty("cache", QString());
ticket->setProperty("cachetimebase", QVariant::fromValue(olive::rational(1)));
ticket->setProperty("cacheid", QVariant::fromValue(QUuid()));
ticket->setProperty("multicam", olive::QtUtils::PtrToValue(static_cast<void *>(nullptr)));
ticket->Start();
olive::RenderProcessor::Process(ticket, renderer_, nullptr, &shader_cache_);
if (!ticket->HasResult()) {
return Write(ErrorMessage(QStringLiteral("render produced no frame"), message.ticket_id));
}
olive::FramePtr frame = ticket->Get().value<olive::FramePtr>();
if (!frame || !frame->is_allocated()) {
return Write(ErrorMessage(QStringLiteral("render result was empty"), message.ticket_id));
}
uint32_t slot = 0;
if (!output_pool_->Acquire(&slot)) {
return Write(ErrorMessage(QStringLiteral("no free output frame slot"), message.ticket_id));
}
const int data_size = frame->allocated_size();
if (data_size > int(output_pool_->slot_data_bytes())) {
output_pool_->Release(slot);
return Write(ErrorMessage(QStringLiteral("rendered frame does not fit output slot"),
message.ticket_id));
}
std::memcpy(output_pool_->SlotData(slot), frame->const_data(), size_t(data_size));
olive::ipc::FrameSlotMeta *meta = output_pool_->Meta(slot);
meta->id = message.ticket_id;
meta->time_num = frame->timestamp().numerator();
meta->time_den = frame->timestamp().denominator();
meta->width = frame->width();
meta->height = frame->height();
meta->format = int32_t(frame->format());
meta->channel_count = frame->channel_count();
meta->linesize = frame->linesize_bytes();
meta->data_size = data_size;
if (!output_pool_->Publish(slot)) {
output_pool_->Release(slot);
return Write(ErrorMessage(QStringLiteral("failed to publish output frame slot"),
message.ticket_id));
}
olive::ipc::FrameReadyMsg ready;
ready.ticket_id = message.ticket_id;
ready.output_slot = int(slot);
return Write(ready.ToJson());
}
olive::OpenGLRenderer *renderer_;
QFile *out_;
bool shutdown_requested_ = false;
std::unique_ptr<olive::Project> project_;
QHash<QString, olive::Node *> node_by_token_;
olive::ipc::SharedMemoryRegion output_region_;
std::optional<olive::ipc::FrameSlotPool> output_pool_;
olive::ShaderCache shader_cache_;
};
} // namespace
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts);
InstallSurfaceFormat();
QGuiApplication app(argc, argv);
QCoreApplication::setOrganizationName(QStringLiteral("oakvideoeditor.org"));
QCoreApplication::setApplicationName(QStringLiteral("olive-render-worker"));
QFile in;
QFile out;
if (!in.open(stdin, QIODevice::ReadOnly | QIODevice::Unbuffered) ||
!out.open(stdout, QIODevice::WriteOnly | QIODevice::Unbuffered)) {
LogError(QStringLiteral("failed to open stdio control pipes"));
return 1;
}
auto *renderer = new olive::OpenGLRenderer();
if (!renderer->Init()) {
LogError(QStringLiteral("failed to initialize OpenGL renderer"));
delete renderer;
return 1;
}
renderer->PostInit();
QOpenGLContext *ctx = renderer->context();
if (!ctx || !ctx->isValid()) {
LogError(QStringLiteral("OpenGL context is not valid after init"));
renderer->Destroy();
renderer->PostDestroy();
delete renderer;
return 1;
}
int exit_code = 0;
{
RenderWorker worker(renderer, &out);
if (!worker.InitializeRuntime() || !worker.SendStartupHandshake()) {
exit_code = 1;
} else {
QByteArray buffer;
while (!worker.shutdown_requested() && !in.atEnd()) {
const QByteArray chunk = in.readLine();
if (chunk.isEmpty()) {
break;
}
buffer.append(chunk);
while (true) {
QJsonObject message;
bool ok = true;
if (!olive::ipc::ReadMessage(&buffer, &message, &ok)) {
if (!ok) {
olive::ipc::WriteMessage(
&out, ErrorMessage(QStringLiteral("malformed control message")));
out.flush();
continue;
}
break;
}
if (!worker.Handle(message)) {
exit_code = 1;
break;
}
}
}
}
}
renderer->Destroy();
renderer->PostDestroy();
delete renderer;
return exit_code;
}
@@ -1,872 +0,0 @@
# Olive/Oak 模块化与多进程渲染架构方案
> **状态**:设计文档(Design Doc
> **范围**:仅制定方案,不涉及代码变更。
> **目标**:将当前单体架构拆分为多个动态库,并将渲染引擎改造为独立进程,通过 stdio 进行 IPC 通信。
---
## 1. 概述
当前 Olive/Oak 采用**单体编译模型**:所有业务代码被聚合到 `libolive-editor`OBJECT 库),最终链接为单个 `olive-editor` 可执行文件。这种架构在项目规模较小时工作良好,但随着 OFX 插件、节点图复杂度、多轨道高清/超高清处理的加入,单体架构面临以下问题:
- **编译-链接耗时**:任何小改动都触发大规模重编译和重链接。
- **渲染崩溃导致编辑器全崩**OpenGL/OFX/FFmpeg 的崩溃会直接拖垮整个 GUI 进程,用户未保存的工作全部丢失。
- **插件隔离性差**:OFX 插件与主程序共享地址空间,恶意或 buggy 插件可任意破坏内存。
- **可扩展性受限**:未来如要支持分布式渲染、云渲染、独立批处理工具,均需先打破单体边界。
本方案提出**两阶段架构演进**
1. **动态库拆分**:按功能层次将代码拆分为若干共享库(`.so`/`.dylib`/`.dll`),明确模块边界与符号可见性。
2. **渲染器多进程化**:将 `render/` 相关逻辑从主进程剥离为独立可执行文件 `olive-renderer`,主进程通过 **stdiostdin/stdout** 进行 IPC 控制,**共享内存/内存映射文件**传输大帧数据。
---
## 2. 总体架构目标
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ olive-editor(主进程,GUI
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ liboliveui │ │ libolivenode │ │libolivecodec │ │ liboliverender │ │
│ │ (widget/ │ │ (node/ │ │ (codec/ │ │ -client (轻量) │ │
│ │ panel/ │ │ timeline/ │ │ common/) │ │ IPC 封装层 │ │
│ │ window/) │ │ undo/) │ │ │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └────────┬────────┘ │
│ ▲ ▲ ▲ │ │
│ └─────────────────┴─────────────────┘ │ QProcess │
│ 动态链接 │ stdin/stdout│
├──────────────────────────────────────────────────────────────────┼───────────┤
│ │ │
│ ┌───────────────────────────────────────────────────────────────┘ │
│ │ olive-renderer(子进程,无 GUI
│ │ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ RenderService (IPC 服务端,监听 stdin,输出 stdout) │ │
│ │ └──────────────────────────┬──────────────────────────────────────────┘ │
│ │ │ │
│ │ ┌──────────────────────────▼──────────────────────────────────────────┐ │
│ │ │ RenderProcessor + OpenGLRenderer + DecoderCache + ShaderCache │ │
│ │ │ (原有渲染逻辑,在独立地址空间运行,崩溃不影响主进程) │ │
│ │ └─────────────────────────────────────────────────────────────────────┘ │
│ └────────────────────────────────────────────────────────────────────────────┘
│ 基础依赖(所有模块共享):
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
│ │ libolivecore│ │liboliveaudio│ │ liboliveplugin │
│ │ (ext/core) │ │ (audio/) │ │ (pluginSupport/) │
│ └─────────────┘ └─────────────┘ └──────────────────┘
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 3. 动态库拆分方案
### 3.1 拆分原则
- **低侵入性**:优先拆分依赖关系清晰、接口明确的模块;对耦合严重的 `node/``render/` 暂不强行物理分割,而是通过**接口抽象 + 动态链接**降低耦合。
- **分层依赖**:严格遵循 `上层 → 下层` 的依赖方向,禁止循环依赖。
- **符号可控**:引入 `OLIVE_<MODULE>_API` 宏,显式导出公共接口,隐藏内部符号(`-fvisibility=hidden`)。
- **Qt 元对象系统兼容**:跨动态库的 Qt 信号/槽需确保 `moc` 生成的元对象信息可被正确链接,推荐在公共头文件中完整声明信号/槽。
### 3.2 库划分
| 动态库 | 包含源码 | 外部依赖 | 说明 |
|---|---|---|---|
| `libolivecore.so` | `ext/core/` | FFmpeg::avutil, OpenGL::GL, Imath | 已有独立库,仅将构建类型由 `STATIC` 改为 `SHARED`。 |
| `libolivecodec.so` | `app/codec/`, `app/common/` | olivecore, FFmpeg, OpenImageIO, OpenEXR | 编解码 + 通用工具。`common/` 因被 `codec/` 重度依赖且不含 UI,故合并。 |
| `liboliveplugin.so` | `app/pluginSupport/`, `third_party/openfx/HostSupport` | olivecore, Qt::Core, expat | OFX 宿主支持,相对独立。 |
| `liboliveaudio.so` | `app/audio/` | olivecore, PortAudio, Qt::Core | 音频播放管理。 |
| `libolivenode.so` | `app/node/`, `app/timeline/`, `app/undo/`, `app/config/` | olivecore, olivecodec, Qt::Core | **核心数据层**。节点图、时间线模型、Undo、配置。注意:当前 `Node.h` 包含部分 `render/` 头文件(缓存类型、作业枚举),需先进行**头文件解耦**(见 3.4)。 |
| `liboliverender.so` | `app/render/`(不含 OpenGL 具体后端) | olivenode, olivecodec, olivecore, Qt::Core, OpenColorIO | 渲染抽象层:`Renderer`, `RenderProcessor`, `RenderTicket`, `Job` 体系。 |
| `liboliveui.so` | `app/widget/`, `app/panel/`, `app/window/`, `app/dialog/`, `app/tool/`, `app/ui/` | olivenode, oliverender, olivecodec, olivecore, oliveaudio, Qt::Widgets, KDDockWidgets | **UI 层**。所有 Qt Widget 相关代码。 |
| `libolivetask.so` | `app/task/` | olivenode, olivecodec, oliverender, olivecore, Qt::Core | 任务调度系统。可独立成库,也可在初期并入 `liboliveui.so`。 |
| `olive-editor` | `main.cpp`, `core.cpp/h` | 上述全部 | 主可执行文件,仅保留入口和全局生命周期管理。 |
### 3.3 依赖关系图
```
┌─────────────┐
│ olive-editor│
└──────┬──────┘
│ links all
┌──────────────────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌─────────────┐ ┌─────────────┐
│liboliveui│ │libolivetask │ │liboliverender│
└────┬────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
└────────────────────┼──────────────────────┘
┌─────────────┐
│ libolivenode│
└──────┬──────┘
┌───────────────┼───────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│liboliveaudio│ │libolivecodec│ │liboliveplugin│
└──────┬──────┘ └──────┬──────┘ └─────────────┘
│ │
└───────────────┼───────────────┘
┌─────────────┐
│libolivecore │
└─────────────┘
```
**依赖规则**
- 禁止任何下层库依赖上层库。
- `libolivenode.so` 当前依赖 `render/` 的部分类型(`FrameHashCache`, `ShaderJob` 等枚举),需通过**前向声明(forward declare**或**接口抽象**解耦。
### 3.4 接口与符号可见性
#### 3.4.1 导出宏定义
在每个模块的公共头目录(如 `app/node/api.h`)中定义:
```cpp
// app/node/api.h
#pragma once
#include "common/define.h"
#ifdef OLIVE_BUILDING_NODE
# define OLIVE_NODE_API Q_DECL_EXPORT
#else
# define OLIVE_NODE_API Q_DECL_IMPORT
#endif
```
所有需要跨库使用的类/函数均标记:
```cpp
// app/node/node.h
class OLIVE_NODE_API Node : public QObject { ... };
```
#### 3.4.2 Node 与 Render 的解耦
当前 `Node.h` 包含以下 render 头文件(经代码分析):
- `render/rendercache.h``FrameHashCache` 等)
- `render/job/shaderjob.h` 等(作业类型)
**解耦策略**
1. **枚举与前置声明**:将 `RenderTicket::ReturnType`, `RenderMode::Mode`, `PixelFormat` 等移到 `ext/core/``common/` 中,使其不依赖 `render/`
2. **接口回调**`Node` 中需要通知缓存失效的逻辑,改为通过 `NodeCacheInterface` 纯虚接口注入,而非直接引用 `FrameHashCache`
3. **Job 类型**`Node` 仅需要知道 `ShaderJob` 等类型的存在以支持虚函数分发,可以将 `ProcessShader` 等虚函数的参数从具体类型改为更抽象的 `const void *` 或基类指针,或把 `render/job/*.h` 中仅含数据定义的头文件移动到 `common/`
### 3.5 CMake 改造要点
当前 `app/` 下的子模块通过修改 `PARENT_SCOPE` 变量 `OLIVE_SOURCES` 来汇报源文件,最终由 `app/CMakeLists.txt` 统一创建 `libolive-editor` OBJECT 库。改造后,每个子模块应自行产出库目标。
#### 3.5.1 子模块 CMakeLists.txt 改造示例
`app/node/CMakeLists.txt` 为例:
```cmake
# 改造前:仅收集源文件到 PARENT_SCOPE
# set(OLIVE_SOURCES ${OLIVE_SOURCES} node.cpp node.h PARENT_SCOPE)
# 改造后:创建本模块的 OBJECT/SHARED 库片段
add_subdirectory(project)
add_subdirectory(output)
# ... 其他子目录
set(NODE_SOURCES
node.cpp
node.h
traverser.cpp
traverser.h
# ... 所有 node/ 下源文件
)
# 方案 A:本模块建 SHARED 库(推荐)
add_library(olivenode SHARED ${NODE_SOURCES})
target_link_libraries(olivenode
PUBLIC
olivecore
olivecodec
Qt${QT_VERSION_MAJOR}::Core
)
target_compile_definitions(olivenode PRIVATE OLIVE_BUILDING_NODE)
target_include_directories(olivenode
PUBLIC
$<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/app>
$<INSTALL_INTERFACE:include>
)
# 方案 B:本模块建 OBJECT 库,由上层组合为 SHARED 库
# add_library(olivenode-obj OBJECT ${NODE_SOURCES})
# ... 在 app/CMakeLists.txt 中组合
```
#### 3.5.2 顶层 app/CMakeLists.txt 改造
```cmake
# 各子模块自行创建库目标
add_subdirectory(audio) # -> oliveaudio
add_subdirectory(codec) # -> olivecodec
add_subdirectory(common) # -> 并入 olivecodec 或单独 common
add_subdirectory(config) # -> 并入 olivenode
add_subdirectory(node) # -> olivenode
add_subdirectory(render) # -> oliverender
add_subdirectory(task) # -> olivetask
add_subdirectory(timeline) # -> 并入 olivenode
add_subdirectory(undo) # -> 并入 olivenode
add_subdirectory(widget) # -> oliveui
add_subdirectory(panel) # -> oliveui
add_subdirectory(window) # -> oliveui
add_subdirectory(dialog) # -> oliveui
add_subdirectory(tool) # -> oliveui
add_subdirectory(ui) # -> oliveui
add_subdirectory(pluginSupport) # -> oliveplugin
# 版本对象保持 OBJECT
add_library(olive-version-obj OBJECT version.cpp version.h)
target_link_libraries(olive-version-obj PRIVATE Qt${QT_VERSION_MAJOR}::Core)
# 主可执行文件
add_executable(olive-editor
main.cpp
core.cpp
core.h
$<TARGET_OBJECTS:olive-version-obj>
)
target_link_libraries(olive-editor PRIVATE
oliveui
olivetask
oliverender
olivenode
oliveaudio
olivecodec
oliveplugin
olivecore
# ... 外部依赖
)
```
#### 3.5.3 平台注意事项
- **Windows**`Q_DECL_EXPORT`/`Q_DECL_IMPORT` 会自动处理 `__declspec(dllexport/dllimport)`。需确保 `olive-editor.exe` 与所有 `.dll` 在同一目录,或通过 `PATH` 找到。
- **macOS**:动态库后缀为 `.dylib`。若打包为 `.app` Bundle,需使用 `install_name_tool` 或 CMake 的 `@rpath` 设置确保加载路径正确。
- **Linux**:使用 `RPATH``LD_LIBRARY_PATH`。打包时可用 `patchelf` 或 AppImage 工具。
---
## 4. 渲染器多进程化方案
### 4.1 进程模型
| 进程 | 职责 | 技术栈 |
|---|---|---|
| `olive-editor`(主进程) | GUI、项目数据管理、时间线编辑、用户交互 | Qt Widgets, KDDockWidgets |
| `olive-renderer`(子进程) | 节点图遍历、GPU/OpenGL 渲染、FFmpeg 解码、OFX 插件执行 | Qt Core(非 GUI, OpenGL, FFmpeg, OCIO |
**启动方式**:主进程通过 `QProcess` 启动 `olive-renderer`,并捕获其 `stdin/stdout` 作为通信管道。子进程不使用任何 GUI 模块,仅初始化 `QCoreApplication` 和 OpenGL 离屏上下文。
### 4.2 IPC 通信协议(stdio
#### 4.2.1 传输格式:NDJSON
采用 **Newline Delimited JSONNDJSON**,每行一条完整 JSON 消息,以 `\n` 分隔。理由:
- 基于文本,易于调试(`echo '{...}' | olive-renderer` 可手动测试)。
- 结构化,易于扩展新字段。
- 帧边界天然由换行符确定,无需额外的长度前缀或帧同步协议。
#### 4.2.2 消息定义
**请求消息(主进程 → 子进程,写入子进程 stdin)**
| 字段 | 类型 | 说明 |
|---|---|---|
| `type` | string | 消息类型:`init`, `render_frame`, `render_audio`, `cancel`, `shutdown`, `ping` |
| `req_id` | int | 请求唯一标识,用于响应匹配。 |
| `...` | 类型相关 | 见下表。 |
**响应消息(子进程 → 主进程,写入子进程 stdout)**
| 字段 | 类型 | 说明 |
|---|---|---|
| `type` | string | 消息类型:`ready`, `result`, `error`, `cancelled`, `heartbeat`, `pong` |
| `req_id` | int | 对应请求的 `req_id`。对于主动推送(如 `heartbeat`),`req_id``0`。 |
| `...` | 类型相关 | 见下表。 |
#### 4.2.3 详细消息格式
```json
// === 初始化 ===
// 主进程 -> 子进程
{"type":"init","req_id":1,"backend":"opengl","shader_path":"/usr/share/olive/shaders","ocio_config_path":"/path/to/config.ocio"}
// 子进程 -> 主进程
{"type":"ready","req_id":1,"status":"ok","backend_version":"4.6 (Core Profile)"}
{"type":"error","req_id":1,"status":"error","message":"Failed to create OpenGL context"}
// === 渲染视频帧 ===
// 主进程 -> 子进程
{
"type": "render_frame",
"req_id": 2,
"ticket_id": 101,
"node_graph_ref": "a3f7b2d9",
"time": "1001/30000",
"video_params": {
"width": 1920,
"height": 1080,
"depth": 1,
"format": "rgba32f",
"channel_count": 4,
"pixel_aspect": "1/1"
},
"audio_params": {
"sample_rate": 48000,
"channel_layout": "stereo"
},
"mode": "offline",
"color_manager": {
"reference_space": "ACES - ACES2065-1",
"display_space": "Rec.709"
},
"shm_name": "/olive_r_12345_2",
"shm_size": 33177600
}
// 子进程 -> 主进程(成功)
{
"type": "result",
"req_id": 2,
"ticket_id": 101,
"status": "ok",
"shm_name": "/olive_r_12345_2",
"width": 1920,
"height": 1080,
"format": "rgba32f",
"pixel_format_id": 28,
"timestamp_ms": 45
}
// 子进程 -> 主进程(失败)
{
"type": "error",
"req_id": 2,
"ticket_id": 101,
"status": "error",
"category": "decoder",
"message": "Failed to decode frame at time 1001/30000: codec not found"
}
// === 渲染音频 ===
// 主进程 -> 子进程
{
"type": "render_audio",
"req_id": 3,
"ticket_id": 102,
"node_graph_ref": "a3f7b2d9",
"range": {"in": "0/1", "out": "48000/48000"},
"audio_params": {
"sample_rate": 48000,
"channel_layout": "stereo",
"format": "flt_planar"
},
"mode": "offline",
"shm_name": "/olive_r_12345_3",
"shm_size": 384000
}
// === 取消任务 ===
{"type":"cancel","req_id":4,"ticket_id":101}
{"type":"cancelled","req_id":4,"ticket_id":101}
// === 心跳与探测 ===
{"type":"ping","req_id":5}
{"type":"pong","req_id":5}
// 子进程主动心跳(每 3 秒)
{"type":"heartbeat","req_id":0,"timestamp":1716288000}
// === 优雅退出 ===
{"type":"shutdown","req_id":6}
{"type":"result","req_id":6,"status":"ok"}
```
#### 4.2.4 通信时序示例
```
主进程 子进程 (olive-renderer)
│ │
│── QProcess::start() ─────────────────>│
│ │ 初始化 Qt Core
│ │ 初始化 OpenGL 上下文
│<── stdout: {"type":"ready",...} ─────│
│ │
│── stdin: render_frame (ticket #1) ──>│
│── stdin: render_frame (ticket #2) ──>│ 入队渲染
│ │
│<── stdout: result (ticket #1) ───────│ 完成帧 #1
│── 读取共享内存帧数据 │
│── shm_unlink() │
│ │
│<── stdout: result (ticket #2) ───────│ 完成帧 #2
│ │
│── stdin: cancel (ticket #3) ────────>│ 取消正在进行的 #3
│<── stdout: cancelled (ticket #3) ────│
│ │
│── stdin: shutdown ──────────────────>│ 清理资源,退出事件循环
│<── stdout: result (shutdown) ────────│
│── QProcess::waitForFinished() ──────>│ 进程结束
```
### 4.3 数据平面:共享内存帧传输
NDJSON 仅适合传输控制命令和元数据。**视频帧(RGBA32F, 1920×1080 ≈ 33MB)和音频块**不能通过 base64 编码在 JSON 中传输(实时预览需要 2430fps,stdio 带宽和 CPU 编解码开销均不可接受)。
#### 4.3.1 方案:POSIX / Windows 共享内存
**POSIXLinux/macOS**
```cpp
// 主进程创建
int fd = shm_open("/olive_r_12345_2", O_RDWR | O_CREAT, 0666);
ftruncate(fd, shm_size);
void *ptr = mmap(nullptr, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 子进程打开(通过 shm_name 从 JSON 中读取)
int fd = shm_open("/olive_r_12345_2", O_RDWR, 0666);
void *ptr = mmap(nullptr, shm_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 使用完毕后,主进程负责 unlink
shm_unlink("/olive_r_12345_2");
```
**Windows**
```cpp
// 主进程创建
HANDLE hMap = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE,
0, shm_size, L"Local\\olive_r_12345_2");
void *ptr = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, shm_size);
// 子进程打开
HANDLE hMap = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"Local\\olive_r_12345_2");
void *ptr = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, shm_size);
// 清理
UnmapViewOfFile(ptr);
CloseHandle(hMap);
```
**帧数据布局**:共享内存前 256 字节保留为**元数据头**(Magic、版本、实际数据偏移、行间距 linesize、校验和),后续为原始像素/采样数据。
```
┌─────────────────────┬──────────────────────────────────────┐
│ Header (256 B) │ Pixel Data │
│ magic | offset | │ row 0 │ row 1 │ ... │ row H-1 │
│ linesize | ... │ (width * channels * sizeof(float)) │
└─────────────────────┴──────────────────────────────────────┘
```
#### 4.3.2 备选方案:内存映射临时文件
若共享内存 API 在不同平台间行为不一致,可退而求其次使用**内存映射临时文件**:
```cpp
QTemporaryFile tmp;
tmp.setFileTemplate("olive_render_XXXXXX.raw");
tmp.open();
tmp.resize(shm_size);
// 内存映射
tmp.seek(0);
uchar *ptr = tmp.map(0, shm_size);
// 将文件路径(而非 shm_name)通过 JSON 传递
{"shm_path": "/tmp/olive_render_a1b2c3.raw", ...}
```
此方案兼容性最好,但性能略低于纯共享内存(因可能触发文件系统页缓存回写)。
### 4.4 节点图序列化与缓存
渲染请求的核心输入是**节点图(Node Graph)**。直接每次传输完整 XML 序列化在实时预览场景下(24–30fps)不可接受。
#### 4.4.1 策略:引用 + 增量更新
1. **首次传输**:当某个 `ViewerOutput` 需要渲染时,主进程将其关联的节点图通过 `type: init_graph` 消息完整序列化发送给子进程,子进程缓存并返回一个 `graph_ref`(如 SHA-256 前 8 位)。
```json
{"type":"init_graph","req_id":10,"graph_ref":"a3f7b2d9","node_graph_xml":"...<Project>...</Project>..."}
{"type":"result","req_id":10,"graph_ref":"a3f7b2d9","node_count":42}
```
2. **后续引用**:渲染帧请求通过 `"node_graph_ref": "a3f7b2d9"` 引用已缓存的图,无需重复传输 XML。
3. **增量更新**:当用户调整某个节点的参数时,主进程发送 `update_graph` 消息,仅携带变更的节点 ID 和参数字段。
```json
{"type":"update_graph","req_id":11,"graph_ref":"a3f7b2d9","updates":[{"node_id":"Transform1","params":{"position":{"x":100,"y":200}}}]}
```
4. **序列化复用**:直接复用现有的 `ProjectSerializer`,以 `kOnlyNodes` 模式序列化目标 `ViewerOutput` 及其上游依赖节点。
#### 4.4.2 子进程中的节点图重建
子进程收到 `init_graph` 后:
1. 使用 `ProjectSerializer::Load()` 将 XML 反序列化为临时 `Project` 对象。
2. 提取目标 `ViewerOutput` 节点,构建本地 `NodeValueDatabase`。
3. 将图对象存入 `graph_ref → Project` 的映射表中。
4. 后续 `render_frame` 直接使用缓存的图,避免重复解析 XML。
### 4.5 渲染进程生命周期管理
#### 4.5.1 启动与就绪
```cpp
// RenderManager::CreateInstance()
render_process_ = new QProcess(this);
render_process_->setProgram(QCoreApplication::applicationDirPath() + "/olive-renderer");
render_process_->setArguments({"--backend", "opengl"});
render_process_->start();
// 等待 ready 消息(带超时)
connect(render_process_, &QProcess::readyReadStandardOutput, this, &RenderManager::ReadStdout);
```
#### 4.5.2 心跳与卡死检测
- 子进程每 **3 秒**主动输出 `heartbeat`。
- 主进程每 **5 秒**发送 `ping`,若 **10 秒**内未收到 `pong`,认为子进程卡死。
- 卡死处理:
1. `render_process_->kill()` 强制终止。
2. 清理所有未完成的 `RenderTicket`,标记为错误状态。
3. 自动重启子进程。
4. 重新发送所有活跃的 `graph_ref` 对应的节点图。
#### 4.5.3 崩溃恢复
```cpp
connect(render_process_, QOverload<QProcess::ProcessError>::of(&QProcess::errorOccurred),
this, [this](QProcess::ProcessError error) {
if (error == QProcess::Crashed) {
qWarning() << "Renderer process crashed. Restarting...";
RestartRendererProcess();
// 通知 UI 显示"渲染器已崩溃并恢复"的提示
}
});
```
**关键收益**:即使 OpenGL 驱动崩溃、OFX 插件 segfault、FFmpeg 解码器触发未处理异常,也只会导致 `olive-renderer` 子进程终止,主进程的 GUI、项目数据、Undo 栈均完好无损。
#### 4.5.4 优雅退出
主进程析构时:
1. 发送 `shutdown` 请求,等待子进程返回 `result`(超时 5 秒)。
2. 若子进程未退出,调用 `terminate()`,再等 3 秒。
3. 若仍未退出,`kill()` 强制结束。
### 4.6 主进程 RenderManager 适配
当前 `RenderManager` 直接创建并管理 `RenderThread`。改造后,`RenderManager` 转型为 **IPC 客户端管理器**,对外接口保持**完全不变**,以最小化 UI 层侵入。
#### 4.6.1 类结构调整
```cpp
// 新增:轻量级 IPC 客户端
class RenderServiceClient : public QObject {
Q_OBJECT
public:
explicit RenderServiceClient(QObject *parent = nullptr);
bool Start(const QString &renderer_executable);
void Shutdown();
// 异步发送渲染请求,返回内部 ticket_id
int RequestRenderFrame(const RenderManager::RenderVideoParams &params, const QString &graph_ref);
int RequestRenderAudio(const RenderManager::RenderAudioParams &params, const QString &graph_ref);
void CancelTicket(int ticket_id);
signals:
void ResultReceived(int ticket_id, const QJsonObject &result);
void ErrorReceived(int ticket_id, const QString &message);
void ProcessCrashed();
void ProcessRecovered();
private:
QProcess *process_;
QHash<int, RenderTicketPtr> ticket_map_; // ticket_id -> RenderTicket
// ...
};
// 改造后的 RenderManager(对外接口不变)
class RenderManager : public QObject {
Q_OBJECT
public:
// 原有接口 100% 保留
RenderTicketPtr RenderFrame(const RenderVideoParams &params);
RenderTicketPtr RenderAudio(const RenderAudioParams &params);
bool RemoveTicket(RenderTicketPtr ticket);
private:
// 旧实现:RenderThread *video_thread_; ...
// 新实现:
RenderServiceClient *client_;
QHash<Node*, QString> node_graph_refs_; // ViewerOutput -> graph_ref
};
```
#### 4.6.2 渲染流程映射
```
UI/Widget 层
│ RenderManager::RenderFrame(params)
RenderManager
│ 1. 检查 params.node 对应的 graph_ref 是否已在子进程缓存
│ 2. 若未缓存,序列化节点图 -> init_graph -> 获取 graph_ref
│ 3. 创建共享内存 -> shm_name
│ 4. 将 params + graph_ref + shm_name 打包为 NDJSON
RenderServiceClient -> QProcess::write() -> 子进程 stdin
│<── stdout: NDJSON result
RenderManager 从共享内存读取帧数据
│ 构造 FramePtr/TexturePtr
│ 调用 RenderTicket::Finish(result)
UI 层收到 RenderTicketWatcher::Finished 信号,更新显示
```
#### 4.6.3 兼容性保留
- 保留 `RenderTicket`, `RenderTicketWatcher`, `RenderTicketPtr` 的完整语义。
- 保留 `PreviewAutoCacher` 的接口,其内部调用 `RenderManager` 的方式无需修改。
- 保留 `RenderMode::Mode`, `ReturnType` 等枚举定义位置,或在 `ext/core/` 中建立同义定义。
---
## 5. 实施路线图
### 5.1 第一阶段:动态库基础拆分(预估 2–3 周)
**目标**:完成低耦合模块的动态库化,建立符号导出规范和 CMake 新范式。
| 任务 | 说明 |
|---|---|
| T1.1 | 将 `ext/core/` 的构建类型由 `STATIC` 改为 `SHARED`,验证所有平台加载正常。 |
| T1.2 | 创建 `libolivecodec.so`:合并 `app/codec/` + `app/common/`,处理跨平台符号导出。 |
| T1.3 | 创建 `liboliveplugin.so`:将 `pluginSupport/` + `OfxHost` 独立,验证 OFX 插件加载。 |
| T1.4 | 创建 `liboliveaudio.so`:将 `app/audio/` 独立。 |
| T1.5 | 引入 `OLIVE_API` 宏体系,为每个模块定义导出/导入宏。 |
| T1.6 | 在 CI 中增加动态库加载路径测试,确保 `olive-editor` 能在干净环境中启动。 |
**里程碑**`olive-editor` 可正常启动,所有原有功能不变,但内部已由 1 个 OBJECT 库变为 4+ 个动态库。
### 5.2 第二阶段:核心层拆分与渲染进程化(预估 4–6 周)
**目标**:完成 `node/` 与 `render/` 的解耦,并实现 `olive-renderer` 子进程。
| 任务 | 说明 |
|---|---|
| T2.1 | **Node/Render 解耦**:将 `Node.h` 中对 `render/` 的包含移除,迁移依赖类型到 `common/` 或 `ext/core/`;将 `FrameHashCache` 交互改为接口注入。 |
| T2.2 | 创建 `libolivenode.so`:包含 `node/`, `timeline/`, `undo/`, `config/`。 |
| T2.3 | 创建 `liboliverender.so`:包含 `render/`(不含 OpenGL 后端具体平台代码),依赖 `olivenode` + `olivecodec`。 |
| T2.4 | 创建 `olive-renderer` 可执行文件目标,复用 `liboliverender.so` + `libolivenode.so` + `libolivecodec.so` + `libolivecore.so`。 |
| T2.5 | 实现 **NDJSON IPC 协议**:在子进程中实现 `RenderService`(基于 `QSocketNotifier` 监听 `stdin`);在主进程中实现 `RenderServiceClient`。 |
| T2.6 | 实现 **共享内存帧传输**:封装 `SharedMemoryBuffer` 类,支持 POSIX + Windows API,统一为 `Create/Attach/Detach/Destroy` 接口。 |
| T2.7 | 实现 **节点图序列化缓存**:在 `RenderServiceClient` 中维护 `graph_ref` 映射表;在子进程中维护反序列化后的节点图缓存。 |
| T2.8 | 改造 `RenderManager`:将内部 `RenderThread` 调度替换为 `RenderServiceClient` IPC 调用,对外接口保持不变。 |
| T2.9 | 实现 **崩溃恢复与心跳**:子进程卡死/崩溃检测,自动重启,重新同步节点图缓存。 |
| T2.10 | 全面回归测试:预览、导出、音频回放、OFX 插件、色彩管理。 |
**里程碑**:渲染在子进程中稳定运行,手动触发子进程崩溃(如 `kill -9`)后,主进程可自动恢复且 GUI 不闪退。
### 5.3 第三阶段:优化与稳定化(预估 2–3 周)
| 任务 | 说明 |
|---|---|
| T3.1 | **增量节点图更新**:实现 `update_graph` 消息,避免参数微调时重复传输完整 XML。 |
| T3.2 | **多渲染进程**:支持同时启动多个 `olive-renderer` 进程(如一个用于预览,一个用于后台导出),提升并行度。 |
| T3.3 | **性能基准测试**:对比单进程 vs 多进程的帧渲染延迟、内存占用、CPU 开销,优化共享内存拷贝次数。 |
| T3.4 | **打包适配**:更新 macOS `.app` Bundle、Windows Installer、Linux AppImage 的打包脚本,确保动态库和 `olive-renderer` 被正确包含。 |
| T3.5 | 文档更新:更新 `build.md`、`build-macos-zh.md`,说明新的运行时依赖和动态库加载路径配置。 |
---
## 6. 风险与对策
| 风险 | 影响 | 对策 |
|---|---|---|
| **Node/Render 解耦工作量超预期** | 高 | 采用"接口抽象 + 前向声明"的轻量解耦,不追求完全消除逻辑耦合,只消除编译期头文件依赖。若实在无法解耦,可将 `node/` + `render/` 暂时合并为一个 `libolive-engine.so`,后续再拆分。 |
| **共享内存跨平台兼容性** | 中 | 封装抽象层 `SharedMemoryBuffer`POSIX 和 Windows 分别实现。若某平台支持不佳,自动降级为内存映射临时文件。 |
| **NDJSON 协议性能瓶颈** | 中 | 控制消息数据量极小(<1KB),不会是瓶颈。若未来需要更高吞吐,可无损升级为 **MessagePack**(二进制 JSON 兼容格式),无需改协议语义。 |
| **子进程启动延迟影响首帧** | 中 | 采用**预启动策略**:主进程启动后立即在后台启动 `olive-renderer`,用户打开项目时渲染器已就绪。 |
| **GPU/OpenGL 上下文跨进程问题** | 中 | 子进程独立创建离屏 OpenGL 上下文(`QOffscreenSurface`),主进程不再直接操作 GL。主进程 UI 显示通过共享内存获取 CPU 帧数据,或使用平台特定的 GL 共享纹理(进阶优化,初期不做)。 |
| **OFX 插件在子进程中的稳定性** | 高 | 这正是多进程架构的收益点。OFX 插件崩溃仅影响子进程,主进程安全。需确保 OFX 插件资源路径通过 `--resource-path` 参数传递给子进程。 |
| **Qt 信号/槽跨动态库** | 低 | Qt 的元对象系统原生支持跨动态库,只要确保 `moc` 编译了含 `Q_OBJECT` 宏的公共头文件,且动态库被正确链接。 |
---
## 7. 附录:完整消息协议定义
### 7.1 请求消息(主进程 → 子进程)
```typescript
// 基础请求结构
interface Request {
type: string;
req_id: number; // >0
}
interface InitRequest extends Request {
type: "init";
backend: "opengl" | "dummy";
shader_path: string;
ocio_config_path?: string;
}
interface InitGraphRequest extends Request {
type: "init_graph";
graph_ref: string; // 主进程生成的图引用 ID
node_graph_xml: string; // 完整的项目 XMLkOnlyNodes 模式)
}
interface UpdateGraphRequest extends Request {
type: "update_graph";
graph_ref: string;
updates: Array<{
node_id: string;
params: Record<string, any>;
}>;
}
interface RenderFrameRequest extends Request {
type: "render_frame";
ticket_id: number;
graph_ref: string;
time: string; // 有理数字符串,如 "1001/30000"
video_params: VideoParams;
audio_params: AudioParams;
mode: "offline" | "online";
color_manager?: ColorManagerInfo;
force_size?: { width: number; height: number };
force_format?: string;
shm_name: string;
shm_size: number;
}
interface RenderAudioRequest extends Request {
type: "render_audio";
ticket_id: number;
graph_ref: string;
range: { in: string; out: string };
audio_params: AudioParams;
mode: "offline" | "online";
generate_waveforms: boolean;
shm_name: string;
shm_size: number;
}
interface CancelRequest extends Request {
type: "cancel";
ticket_id: number;
}
interface ShutdownRequest extends Request {
type: "shutdown";
}
interface PingRequest extends Request {
type: "ping";
}
```
### 7.2 响应消息(子进程 → 主进程)
```typescript
// 基础响应结构
interface Response {
type: string;
req_id: number; // 对应请求,0 表示主动推送
}
interface ReadyResponse extends Response {
type: "ready";
status: "ok" | "error";
backend_version?: string;
message?: string; // 当 status=error 时
}
interface ResultResponse extends Response {
type: "result";
ticket_id?: number;
status: "ok";
// 对于渲染结果
shm_name?: string;
width?: number;
height?: number;
format?: string;
pixel_format_id?: number;
timestamp_ms?: number; // 渲染耗时
}
interface ErrorResponse extends Response {
type: "error";
ticket_id?: number;
status: "error";
category?: "decoder" | "shader" | "plugin" | "system" | "unknown";
message: string;
}
interface CancelledResponse extends Response {
type: "cancelled";
ticket_id: number;
}
interface PongResponse extends Response {
type: "pong";
}
interface HeartbeatResponse extends Response {
type: "heartbeat";
timestamp: number; // Unix timestamp (seconds)
}
```
### 7.3 共享内存布局(二进制)
```c
#define OLIVE_SHM_MAGIC 0x4F4C4956 // 'OLIV'
#define OLIVE_SHM_VERSION 1
struct ShmHeader {
uint32_t magic; // OLIVE_SHM_MAGIC
uint32_t version; // OLIVE_SHM_VERSION
uint32_t data_offset; // 像素/采样数据起始偏移(通常 256)
uint32_t width; // 帧宽(视频)或采样数(音频)
uint32_t height; // 帧高
uint32_t depth; // 3D 纹理深度
uint32_t channel_count; // 通道数
uint32_t pixel_format; // PixelFormat 枚举值
uint32_t linesize; // 每行字节数(可能包含 padding)
uint64_t data_size; // 实际数据字节数
uint64_t checksum; // CRC64(可选校验)
uint8_t reserved[256 - 48]; // 填充至 256 字节
};
// 紧接着 ShmHeader 之后为原始数据
```
---
## 8. 结论
本方案通过**动态库分层拆分**将 Olive/Oak 从单体编译单元演进为模块化架构,显著提升编译效率和代码边界清晰度;通过**渲染器多进程化**将最易崩溃的 GPU/OFX/FFmpeg 逻辑隔离到独立进程,利用 **stdio + NDJSON + 共享内存** 实现低延迟 IPC,从根本上解决"渲染崩溃导致编辑器闪退丢工作"的痛点。
实施上采用**渐进式路线**:先拆分外围低耦合模块建立规范,再攻克核心 Node/Render 解耦与进程化。对外接口(`RenderManager::RenderFrame` 等)保持完全兼容,UI 层无需感知底层架构变化。
-466
View File
@@ -1,466 +0,0 @@
# libolivecore.so — 基础数据类型库
> **依赖**:无(不依赖其他 Olive 模块)
> **外部依赖**FFmpeg::avutil, OpenGL::GL, Imath::Imath
> **当前状态**`ext/core/` 目录,已作为独立静态库编译
> **改造难度**:⭐(最简单)
---
## 1. 当前状态分析
`ext/core/` 是项目中最为独立的模块,包含纯数据类型和数学工具:
| 文件/类 | 职责 |
|---|---|
| `rational.h` | 有理数(帧率、时间) |
| `color.h` | 颜色表示与运算 |
| `timecodefunctions.h` | 时间码格式化 |
| `timerange.h` | 时间范围 `[in, out)` |
| `samplebuffer.h` | 音频采样缓冲区 |
| `pixelformat.h` | 像素格式枚举 |
| `videoparams.h` / `audioparams.h` | 视频/音频参数 |
| `bezier.h` / `math.h` | 数学工具 |
| `stringutils.h` / `value.h` | 字符串与通用值 |
**优势**
- 无 Qt GUI 依赖(仅用 `Qt::Core` 的基础类型)。
- 无项目内其他模块依赖。
- 主要是 PODPlain Old Data)类型和纯函数。
---
## 2. C API 设计
### 2.1 头文件:`c_api/include/olive/core_api.h`
```c
#ifndef OLIVE_CORE_API_H
#define OLIVE_CORE_API_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stddef.h>
#define OLIVE_CORE_API_VERSION 1
/* ========== 导出宏 ========== */
#ifdef OLIVE_BUILDING_CORE
# define OLIVE_CORE_API __attribute__((visibility("default")))
#else
# define OLIVE_CORE_API
#endif
/* ========== 枚举 ========== */
typedef enum {
OLIVE_PIXEL_FMT_INVALID = 0,
OLIVE_PIXEL_FMT_RGBA8,
OLIVE_PIXEL_FMT_RGBA16,
OLIVE_PIXEL_FMT_RGBA32F,
OLIVE_PIXEL_FMT_RGB8,
OLIVE_PIXEL_FMT_YUV420P,
OLIVE_PIXEL_FMT_YUV422P,
OLIVE_PIXEL_FMT_YUV444P,
OLIVE_PIXEL_FMT_COUNT
} OlivePixelFormat;
typedef enum {
OLIVE_SAMPLE_FMT_INVALID = 0,
OLIVE_SAMPLE_FMT_U8,
OLIVE_SAMPLE_FMT_S16,
OLIVE_SAMPLE_FMT_S32,
OLIVE_SAMPLE_FMT_FLT,
OLIVE_SAMPLE_FMT_DBL,
OLIVE_SAMPLE_FMT_U8P,
OLIVE_SAMPLE_FMT_S16P,
OLIVE_SAMPLE_FMT_S32P,
OLIVE_SAMPLE_FMT_FLTP,
OLIVE_SAMPLE_FMT_DBLP,
OLIVE_SAMPLE_FMT_COUNT
} OliveSampleFormat;
typedef enum {
OLIVE_OK = 0,
OLIVE_ERROR_GENERIC = -1,
OLIVE_ERROR_INVALID = -2,
OLIVE_ERROR_NOMEM = -3,
OLIVE_ERROR_NOT_FOUND = -4,
OLIVE_ERROR_IO = -5,
OLIVE_ERROR_CANCELLED = -6,
OLIVE_ERROR_UNSUPPORTED = -7,
} OliveResult;
/* ========== POD 结构体 ========== */
typedef struct {
int64_t num;
int64_t den;
} OliveRational;
typedef struct {
double r;
double g;
double b;
double a;
} OliveColor;
typedef struct {
int width;
int height;
} OliveSize;
typedef struct {
int width;
int height;
int depth;
int channel_count;
OlivePixelFormat format;
double pixel_aspect_num;
double pixel_aspect_den;
} OliveVideoParams;
typedef struct {
int sample_rate;
int64_t channel_layout; // FFmpeg AV_CH_LAYOUT_* 值
OliveSampleFormat format;
} OliveAudioParams;
/* ========== API 版本 ========== */
OLIVE_CORE_API int olive_core_api_version(void);
/* ========== 内存管理 ========== */
OLIVE_CORE_API void olive_core_free(void* ptr);
OLIVE_CORE_API void* olive_core_alloc(size_t size);
OLIVE_CORE_API void* olive_core_realloc(void* ptr, size_t size);
/* ========== 错误处理 ========== */
OLIVE_CORE_API int olive_core_last_error_code(void);
OLIVE_CORE_API const char* olive_core_last_error_string(void);
/* ========== Rational ========== */
OLIVE_CORE_API OliveRational olive_rational_make(int64_t num, int64_t den);
OLIVE_CORE_API OliveRational olive_rational_add(OliveRational a, OliveRational b);
OLIVE_CORE_API OliveRational olive_rational_sub(OliveRational a, OliveRational b);
OLIVE_CORE_API OliveRational olive_rational_mul(OliveRational a, OliveRational b);
OLIVE_CORE_API OliveRational olive_rational_div(OliveRational a, OliveRational b);
OLIVE_CORE_API double olive_rational_to_double(OliveRational r);
OLIVE_CORE_API OliveRational olive_rational_from_double(double v, int64_t max_den);
OLIVE_CORE_API int olive_rational_cmp(OliveRational a, OliveRational b);
OLIVE_CORE_API int olive_rational_is_valid(OliveRational r);
OLIVE_CORE_API void olive_rational_reduce(OliveRational* r);
/* ========== Color ========== */
OLIVE_CORE_API OliveColor olive_color_make(double r, double g, double b, double a);
OLIVE_CORE_API OliveColor olive_color_add(OliveColor a, OliveColor b);
OLIVE_CORE_API OliveColor olive_color_mul_scalar(OliveColor c, double s);
/* ========== TimeRange ========== */
typedef struct OliveTimeRange OliveTimeRange;
OLIVE_CORE_API OliveTimeRange* olive_time_range_create(OliveRational in, OliveRational out);
OLIVE_CORE_API void olive_time_range_destroy(OliveTimeRange* tr);
OLIVE_CORE_API OliveRational olive_time_range_in(OliveTimeRange* tr);
OLIVE_CORE_API OliveRational olive_time_range_out(OliveTimeRange* tr);
OLIVE_CORE_API OliveRational olive_time_range_length(OliveTimeRange* tr);
OLIVE_CORE_API int olive_time_range_contains(OliveTimeRange* tr, OliveRational t);
OLIVE_CORE_API int olive_time_range_overlaps(OliveTimeRange* a, OliveTimeRange* b);
/* ========== Timecode ========== */
OLIVE_CORE_API char* olive_timecode_from_rational(OliveRational time,
OliveRational timebase,
int display_mode);
OLIVE_CORE_API OliveRational olive_timecode_to_rational(const char* timecode,
OliveRational timebase);
/* ========== PixelFormat ========== */
OLIVE_CORE_API int olive_pixel_format_bytes_per_channel(OlivePixelFormat fmt);
OLIVE_CORE_API int olive_pixel_format_channel_count(OlivePixelFormat fmt);
OLIVE_CORE_API size_t olive_pixel_format_frame_size(OlivePixelFormat fmt, int width, int height);
OLIVE_CORE_API const char* olive_pixel_format_name(OlivePixelFormat fmt);
/* ========== SampleBuffer ========== */
typedef struct OliveSampleBuffer OliveSampleBuffer;
OLIVE_CORE_API OliveSampleBuffer* olive_sample_buffer_create(OliveAudioParams params,
int sample_count);
OLIVE_CORE_API void olive_sample_buffer_destroy(OliveSampleBuffer* buf);
OLIVE_CORE_API int olive_sample_buffer_sample_count(OliveSampleBuffer* buf);
OLIVE_CORE_API int olive_sample_buffer_channel_count(OliveSampleBuffer* buf);
OLIVE_CORE_API void* olive_sample_buffer_channel_data(OliveSampleBuffer* buf, int channel);
OLIVE_CORE_API size_t olive_sample_buffer_channel_data_size(OliveSampleBuffer* buf);
OLIVE_CORE_API OliveAudioParams olive_sample_buffer_params(OliveSampleBuffer* buf);
OLIVE_CORE_API OliveSampleBuffer* olive_sample_buffer_silence(OliveAudioParams params,
int sample_count);
/* ========== VideoParams 辅助 ========== */
OLIVE_CORE_API size_t olive_video_params_frame_size(OliveVideoParams params);
OLIVE_CORE_API int olive_video_params_is_valid(OliveVideoParams params);
#ifdef __cplusplus
}
#endif
#endif // OLIVE_CORE_API_H
```
### 2.2 实现:`c_api/src/core_api.cpp`
```cpp
#include "olive/core_api.h"
#include <olive/core/core.h>
#include <olive/core/rational.h>
#include <olive/core/color.h>
#include <olive/core/timerange.h>
#include <olive/core/samplebuffer.h>
#include <olive/core/pixelformat.h>
#include <olive/core/timecodefunctions.h>
#include <cstring>
#include <cstdlib>
// 线程局部错误状态
thread_local int g_last_error_code = OLIVE_OK;
thread_local char g_last_error_string[1024];
static void SetError(int code, const char* msg) {
g_last_error_code = code;
strncpy(g_last_error_string, msg, sizeof(g_last_error_string) - 1);
g_last_error_string[sizeof(g_last_error_string) - 1] = '\0';
}
extern "C" {
int olive_core_api_version(void) { return OLIVE_CORE_API_VERSION; }
void olive_core_free(void* ptr) { free(ptr); }
void* olive_core_alloc(size_t size) { return malloc(size); }
void* olive_core_realloc(void* ptr, size_t size) { return realloc(ptr, size); }
int olive_core_last_error_code(void) { return g_last_error_code; }
const char* olive_core_last_error_string(void) { return g_last_error_string; }
OliveRational olive_rational_make(int64_t num, int64_t den) {
return {num, den};
}
OliveRational olive_rational_add(OliveRational a, OliveRational b) {
olive::Rational ra(a.num, a.den);
olive::Rational rb(b.num, b.den);
auto rc = ra + rb;
return {rc.numerator(), rc.denominator()};
}
// ... 其他 rational 运算类似封装 ...
double olive_rational_to_double(OliveRational r) {
return olive::Rational(r.num, r.den).toDouble();
}
OliveColor olive_color_make(double r, double g, double b, double a) {
return {r, g, b, a};
}
OliveTimeRange* olive_time_range_create(OliveRational in, OliveRational out) {
try {
auto* tr = new OliveTimeRange();
// 内部持有 olive::TimeRange 指针
// tr->impl = new olive::TimeRange(...);
return tr;
} catch (...) {
SetError(OLIVE_ERROR_NOMEM, "Failed to create TimeRange");
return nullptr;
}
}
void olive_time_range_destroy(OliveTimeRange* tr) {
if (tr) {
delete tr;
}
}
OliveRational olive_time_range_in(OliveTimeRange* tr) {
// auto r = tr->impl->in();
// return {r.numerator(), r.denominator()};
return {0, 1}; // 占位
}
// ... 其他 TimeRange 封装 ...
char* olive_timecode_from_rational(OliveRational time,
OliveRational timebase,
int display_mode) {
try {
olive::Rational t(time.num, time.den);
olive::Rational tb(timebase.num, timebase.den);
QString str = olive::Timecode::time_to_string(
t, tb,
static_cast<olive::Timecode::Display>(display_mode)
);
QByteArray utf8 = str.toUtf8();
char* result = static_cast<char*>(malloc(utf8.size() + 1));
memcpy(result, utf8.constData(), utf8.size() + 1);
return result;
} catch (...) {
SetError(OLIVE_ERROR_GENERIC, "Timecode conversion failed");
return nullptr;
}
}
OliveSampleBuffer* olive_sample_buffer_create(OliveAudioParams params, int sample_count) {
try {
// olive::AudioParams cpp_params = ...;
auto* buf = new OliveSampleBuffer();
// buf->impl = new olive::SampleBuffer(cpp_params, sample_count);
return buf;
} catch (...) {
SetError(OLIVE_ERROR_NOMEM, "Failed to create SampleBuffer");
return nullptr;
}
}
void olive_sample_buffer_destroy(OliveSampleBuffer* buf) {
if (buf) {
delete buf;
}
}
// ... 其他 SampleBuffer 封装 ...
} // extern "C"
```
---
## 3. CMake 改造
### 3.1 `ext/core/CMakeLists.txt`
```cmake
# 改造前
# add_library(olivecore STATIC ...)
# 改造后
set(CORE_SOURCES
src/rational.cpp
src/color.cpp
src/timerange.cpp
src/samplebuffer.cpp
src/pixelformat.cpp
src/timecodefunctions.cpp
# ... 其他源文件
)
# C API 封装层
set(CORE_API_SOURCES
${CMAKE_SOURCE_DIR}/c_api/src/core_api.cpp
)
add_library(olivecore SHARED
${CORE_SOURCES}
${CORE_API_SOURCES}
)
target_compile_definitions(olivecore PRIVATE OLIVE_BUILDING_CORE)
target_include_directories(olivecore
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
PRIVATE
${CMAKE_SOURCE_DIR}/c_api/include
)
target_link_libraries(olivecore
PUBLIC
FFmpeg::avutil
OpenGL::GL
Imath::Imath
)
# 默认隐藏符号,只有标记 OLIVE_CORE_API 的才导出
set_target_properties(olivecore PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
# 安装
install(TARGETS olivecore DESTINATION lib)
install(DIRECTORY include/olive DESTINATION include)
install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/core_api.h DESTINATION include/olive)
```
---
## 4. 小步快跑实施步骤
### Step 0: 准备基础设施(1 天)
- [ ] 创建 `c_api/` 目录结构(`include/olive/`, `src/`, `tests/`)。
- [ ] 编写 `ModuleLoader` 类(`c_api/src/module_loader.h/cpp`),支持 POSIX + Windows。
- [ ] 在 CMake 中新增 `OLIVE_DYNAMIC_MODULES` 选项(默认 OFF)。
- [ ] 编写最小测试动态库,验证 `ModuleLoader` 可以正确加载和调用。
**验收标准**`ModuleLoader` 可以成功 `dlopen` 一个测试 SO 并调用其中的函数。
### Step 1: 将 olivecore 改为 SHARED1 天)
- [ ] 修改 `ext/core/CMakeLists.txt``STATIC``SHARED`,添加 `CXX_VISIBILITY_PRESET hidden`
- [ ] 为需要导出的类/函数添加导出宏。
- [ ] 验证所有平台编译通过。
**验收标准**`olivecore` 编译为 `.so`/`.dylib`/`.dll`,单元测试通过显式加载运行。
### Step 2: 编写 core C API23 天)
- [ ] 编写 `c_api/include/olive/core_api.h`(先只包含最常用的类型:Rational, Color, TimeRange, PixelFormat)。
- [ ] 编写 `c_api/src/core_api.cpp`,用 C++ 封装现有类的调用,导出纯 C 函数。
- [ ] **原则**:不改变 `ext/core/` 下的任何现有源文件,只在 `c_api/src/` 中新增封装代码。
- [ ] 编写单元测试 `tests/c_api/test_core_api.cpp`
**验收标准**
```cpp
ModuleLoader loader;
loader.Load("core", "./libolivecore.so");
auto make = loader.GetFunction<OliveRational(*)(int64_t,int64_t)>("core", "olive_rational_make");
ASSERT_EQ(make(1, 2).num, 1);
```
### Step 3: 主进程加载验证(1 天)
- [ ]`Core::Start()` 中新增代码:尝试显式加载 `libolivecore.so`,若失败则回退到静态链接模式。
- [ ] 验证主程序启动时 `olivecore` 被正确加载。
**验收标准**:主程序日志输出 `Loaded module: core from /path/to/libolivecore.so`
### Step 4: 扩展 C API 覆盖度(按需)
- [ ] 根据其他模块(codec, node)的需要,逐步在 `core_api.h` 中增加类型(SampleBuffer, VideoParams 等)。
---
## 5. 风险与回退
| 风险 | 对策 |
|---|---|
| `ext/core` 的某些类依赖 Qt 模板,导出 C 接口繁琐 | 优先封装 POD 和简单类,复杂类(如 `SampleBuffer` 的音频重采样)暂时不对外暴露,留在内部使用。 |
| Windows 上 `__declspec(dllexport)``__attribute__((visibility))` 混用 | 定义统一的 `OLIVE_API` 宏,根据平台自动选择。 |
| 性能担忧:C 封装层增加函数调用开销 | `core` 中的操作(有理数运算)本身极快,C 封装的开销(一次函数调用)可忽略。若发现瓶颈,可将热点路径内联到 C API 头文件中(但保持 ABI 稳定)。 |
---
## 6. 与后续模块的协作
`libolivecore.so` 是最底层库,所有其他动态库(`olivecodec`, `olivenode`, `oliverender` 等)都隐式或显式依赖它。
- **隐式依赖**`libolivecodec.so` 在编译时链接 `libolivecore.so`,运行时由操作系统加载器自动解析。
- **显式依赖**:主进程需要显式加载 `libolivecore.so`,然后才能加载依赖它的上层库(虽然操作系统加载器会自动处理 `DT_NEEDED`,但主进程仍需要显式 `dlopen` 以确保错误处理可控)。
**加载顺序**
```cpp
loader.Load("core", path); // 必须先加载
loader.Load("codec", path); // 依赖 core,但操作系统会自动解析
loader.Load("node", path); // 依赖 codec + core
```
@@ -1,359 +0,0 @@
# libolivecodec.so — 编解码库
> **依赖**`libolivecore.so`
> **外部依赖**FFmpeg (avcodec, avformat, avutil, swscale, swresample, avfilter), OpenImageIO, OpenEXR
> **包含源码**`app/codec/`, `app/common/`
> **当前状态**:单体 OBJECT 库的一部分
> **改造难度**:⭐⭐(较简单)
---
## 1. 当前状态分析
`app/codec/` 负责媒体文件的读取与写入,`app/common/` 提供通用工具(FFmpeg 辅助、XML 工具、文件操作等)。两者紧密耦合,且 `common/``codec/` 重度依赖,因此合并为一个动态库。
| 组件 | 说明 |
|---|---|
| `decoder.h/cpp` | 解码器抽象基类 |
| `ffmpeg/ffmpegdecoder` / `ffmpegencoder` | FFmpeg 视频/音频解码编码 |
| `oiio/oiiodecoder` / `oiioencoder` | OpenImageIO 图像序列解码编码 |
| `frame.h/cpp` | CPU 帧数据(`FramePtr` |
| `stream.h` | 媒体流信息 |
| `conformmanager.h/cpp` | 音频格式统一转换 |
| `common/ffmpegutils.h` | FFmpeg 辅助函数 |
| `common/xmlutils.h` | XML 序列化辅助 |
| `common/filefunctions.h` | 文件操作 |
---
## 2. C API 设计
### 2.1 头文件:`c_api/include/olive/codec_api.h`
```c
#ifndef OLIVE_CODEC_API_H
#define OLIVE_CODEC_API_H
#ifdef __cplusplus
extern "C" {
#endif
#include "core_api.h"
#define OLIVE_CODEC_API_VERSION 1
#ifdef OLIVE_BUILDING_CODEC
# define OLIVE_CODEC_API __attribute__((visibility("default")))
#else
# define OLIVE_CODEC_API
#endif
/* ========== 类型前向声明 ========== */
typedef struct OliveDecoder OliveDecoder;
typedef struct OliveEncoder OliveEncoder;
typedef struct OliveFrame OliveFrame;
typedef struct OliveStream OliveStream;
typedef struct OliveMediaInfo OliveMediaInfo;
/* ========== API 版本 ========== */
OLIVE_CODEC_API int olive_codec_api_version(void);
/* ========== MediaInfo(媒体文件信息) ========== */
OLIVE_CODEC_API OliveMediaInfo* olive_media_info_probe(const char* filename);
OLIVE_CODEC_API void olive_media_info_destroy(OliveMediaInfo* info);
OLIVE_CODEC_API int olive_media_info_stream_count(OliveMediaInfo* info);
OLIVE_CODEC_API int olive_media_info_stream_type(OliveMediaInfo* info, int stream_index); // 0=video, 1=audio, 2=subtitle
OLIVE_CODEC_API OliveVideoParams olive_media_info_video_params(OliveMediaInfo* info, int stream_index);
OLIVE_CODEC_API OliveAudioParams olive_media_info_audio_params(OliveMediaInfo* info, int stream_index);
OLIVE_CODEC_API OliveRational olive_media_info_duration(OliveMediaInfo* info);
OLIVE_CODEC_API const char* olive_media_info_codec_name(OliveMediaInfo* info, int stream_index);
/* ========== Decoder ========== */
OLIVE_CODEC_API OliveDecoder* olive_decoder_create(const char* codec_id);
OLIVE_CODEC_API void olive_decoder_destroy(OliveDecoder* decoder);
OLIVE_CODEC_API int olive_decoder_open(OliveDecoder* decoder,
const char* filename,
int stream_index);
OLIVE_CODEC_API void olive_decoder_close(OliveDecoder* decoder);
// 视频解码:解码指定时间的帧
OLIVE_CODEC_API int olive_decoder_decode_video(OliveDecoder* decoder,
OliveRational time,
OliveFrame** out_frame);
// 音频解码:解码指定时间范围的采样
OLIVE_CODEC_API int olive_decoder_decode_audio(OliveDecoder* decoder,
OliveRational start,
OliveRational duration,
OliveSampleBuffer** out_buffer);
// 获取解码器支持的流参数
OLIVE_CODEC_API OliveVideoParams olive_decoder_video_params(OliveDecoder* decoder);
OLIVE_CODEC_API OliveAudioParams olive_decoder_audio_params(OliveDecoder* decoder);
/* ========== Frame ========== */
OLIVE_CODEC_API void olive_frame_destroy(OliveFrame* frame);
OLIVE_CODEC_API int olive_frame_width(OliveFrame* frame);
OLIVE_CODEC_API int olive_frame_height(OliveFrame* frame);
OLIVE_CODEC_API int olive_frame_linesize(OliveFrame* frame);
OLIVE_CODEC_API OlivePixelFormat olive_frame_format(OliveFrame* frame);
OLIVE_CODEC_API void* olive_frame_data(OliveFrame* frame); // 指向像素数据的指针
OLIVE_CODEC_API size_t olive_frame_data_size(OliveFrame* frame);
// 将 Frame 转换为指定的像素格式(内部使用 swscale)
OLIVE_CODEC_API int olive_frame_convert(OliveFrame* src,
OlivePixelFormat dst_format,
OliveFrame** out_frame);
// 从原始数据创建 Frame(用于渲染结果回传)
OLIVE_CODEC_API OliveFrame* olive_frame_from_data(int width,
int height,
OlivePixelFormat format,
const void* data,
int linesize);
/* ========== Encoder ========== */
OLIVE_CODEC_API OliveEncoder* olive_encoder_create(const char* format_name,
const char* codec_name);
OLIVE_CODEC_API void olive_encoder_destroy(OliveEncoder* encoder);
OLIVE_CODEC_API int olive_encoder_open(OliveEncoder* encoder,
const char* filename,
OliveVideoParams vparams,
OliveAudioParams aparams);
OLIVE_CODEC_API int olive_encoder_write_video(OliveEncoder* encoder, OliveFrame* frame);
OLIVE_CODEC_API int olive_encoder_write_audio(OliveEncoder* encoder, OliveSampleBuffer* buffer);
OLIVE_CODEC_API int olive_encoder_close(OliveEncoder* encoder);
/* ========== Conform(音频格式统一) ========== */
OLIVE_CODEC_API int olive_audio_conform(const char* input_filename,
const char* output_filename,
OliveAudioParams target_params);
#ifdef __cplusplus
}
#endif
#endif // OLIVE_CODEC_API_H
```
### 2.2 实现要点
```cpp
// c_api/src/codec_api.cpp
#include "olive/codec_api.h"
#include "codec/decoder.h"
#include "codec/ffmpeg/ffmpegdecoder.h"
#include "codec/frame.h"
#include "codec/encoder.h"
#include "codec/ffmpeg/ffmpegencoder.h"
#include "common/ffmpegutils.h"
struct OliveDecoder {
olive::DecoderPtr impl;
};
struct OliveFrame {
olive::FramePtr impl;
};
// ... 其他不透明指针定义 ...
extern "C" {
OliveDecoder* olive_decoder_create(const char* codec_id) {
try {
auto* d = new OliveDecoder();
// 根据 codec_id 创建对应的解码器实例
// 若 codec_id 为 nullptr 或 "auto",则自动探测
d->impl = olive::Decoder::CreateFromID(QString::fromUtf8(codec_id));
return d;
} catch (...) {
return nullptr;
}
}
void olive_decoder_destroy(OliveDecoder* decoder) {
delete decoder;
}
int olive_decoder_open(OliveDecoder* decoder, const char* filename, int stream_index) {
if (!decoder || !filename) return OLIVE_ERROR_INVALID;
try {
bool ok = decoder->impl->Open(QString::fromUtf8(filename), stream_index);
return ok ? OLIVE_OK : OLIVE_ERROR_GENERIC;
} catch (...) {
return OLIVE_ERROR_GENERIC;
}
}
int olive_decoder_decode_video(OliveDecoder* decoder, OliveRational time, OliveFrame** out_frame) {
if (!decoder || !out_frame) return OLIVE_ERROR_INVALID;
try {
olive::Rational t(time.num, time.den);
olive::FramePtr frame = decoder->impl->RetrieveVideo(t);
if (!frame) return OLIVE_ERROR_NOT_FOUND;
auto* f = new OliveFrame();
f->impl = frame;
*out_frame = f;
return OLIVE_OK;
} catch (...) {
return OLIVE_ERROR_GENERIC;
}
}
// ... 其他函数类似封装 ...
} // extern "C"
```
---
## 3. CMake 改造
### 3.1 `app/codec/CMakeLists.txt`
```cmake
# 收集 codec/ 内部源文件
set(CODEC_INTERNAL_SOURCES
decoder.cpp decoder.h
encoder.cpp encoder.h
frame.cpp frame.h
stream.cpp stream.h
conformmanager.cpp conformmanager.h
ffmpeg/ffmpegdecoder.cpp ffmpeg/ffmpegdecoder.h
ffmpeg/ffmpegencoder.cpp ffmpeg/ffmpegencoder.h
oiio/oiiodecoder.cpp oiio/oiiodecoder.h
oiio/oiioencoder.cpp oiio/oiioencoder.h
# ...
)
# 收集 common/ 源文件(并入 codec 库)
set(COMMON_INTERNAL_SOURCES
../common/ffmpegutils.cpp ../common/ffmpegutils.h
../common/xmlutils.cpp ../common/xmlutils.h
../common/filefunctions.cpp ../common/filefunctions.h
../common/qtutils.cpp ../common/qtutils.h
../common/debug.cpp ../common/debug.h
# ...
)
# C API 封装层
set(CODEC_API_SOURCES
${CMAKE_SOURCE_DIR}/c_api/src/codec_api.cpp
)
add_library(olivecodec SHARED
${CODEC_INTERNAL_SOURCES}
${COMMON_INTERNAL_SOURCES}
${CODEC_API_SOURCES}
)
target_compile_definitions(olivecodec PRIVATE OLIVE_BUILDING_CODEC)
target_include_directories(olivecodec
PRIVATE
${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/c_api/include
PUBLIC
$<INSTALL_INTERFACE:include>
)
target_link_libraries(olivecodec
PUBLIC
olivecore
FFMPEG::avcodec
FFMPEG::avformat
FFMPEG::avutil
FFMPEG::swscale
FFMPEG::swresample
FFMPEG::avfilter
${OIIO_LIBRARIES}
${OPENEXR_LIBRARIES}
)
set_target_properties(olivecodec PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
install(TARGETS olivecodec DESTINATION lib)
install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/codec_api.h DESTINATION include/olive)
```
---
## 4. 小步快跑实施步骤
### Step 0: 分析依赖关系(半天)
- [ ] 梳理 `app/codec/``app/common/` 中的所有文件。
- [ ] 确认 `common/` 中不包含任何 Qt GUI 相关代码(若有,移出到 `liboliveui.so`)。
- [ ] 列出 `codec/``common/``node/` 的反向依赖(理论上不应有,若有需先解耦)。
**验收标准**:确认 `codec/` + `common/` 的依赖图只包含 `ext/core/`、FFmpeg、OIIO、Qt::Core。
### Step 1: 合并 common 到 codec 库(1 天)
- [ ] 修改 `app/codec/CMakeLists.txt`,将 `app/common/` 的源文件并入。
- [ ]`app/common/CMakeLists.txt` 改为空文件(或删除,保留 add_subdirectory 空壳以兼容)。
- [ ] 确保编译产物为 `libolivecodec.so`(或 `.dylib`/`.dll`)。
**验收标准**`libolivecodec.so` 编译成功,原有单元测试通过。
### Step 2: 设计 C API 的最小子集(1 天)
- [ ] 先只实现渲染流程**最必需**的接口:
- `olive_decoder_create/open/destroy`
- `olive_decoder_decode_video`
- `olive_frame_width/height/data/destroy`
- `olive_media_info_probe`
- [ ] 暂不实现:Encoder、Conform、音频解码的复杂场景。
**验收标准**:可以用 C API 打开一个视频文件并解码出一帧。
### Step 3: 编写 C API 实现(2 天)
- [ ] 编写 `c_api/include/olive/codec_api.h`(最小子集)。
- [ ] 编写 `c_api/src/codec_api.cpp`
- [ ] 每个函数用 `try/catch(...)` 包裹,异常转换为 `OLIVE_ERROR_GENERIC`
- [ ]`c_api/tests/test_codec_api.cpp` 中编写测试。
**验收标准**
```cpp
OliveDecoder* d = olive_decoder_create(nullptr);
olive_decoder_open(d, "test.mp4", 0);
OliveFrame* f = nullptr;
olive_decoder_decode_video(d, olive_rational_make(0, 1), &f);
assert(f != nullptr);
assert(olive_frame_width(f) > 0);
olive_frame_destroy(f);
olive_decoder_destroy(d);
```
### Step 4: 显式加载验证(1 天)
- [ ] 在主进程中通过 `ModuleLoader` 加载 `libolivecodec.so`
- [ ] 验证可以成功解码测试视频并显示帧尺寸。
**验收标准**:主进程日志输出成功加载 `codec`,并能获取测试视频的宽和高。
### Step 5: 扩展 C API(按需迭代)
- [ ] 根据 `node/``render/` 的需要,逐步增加 Encoder、音频解码、Conform 等接口。
- [ ] 每次增加后运行编解码单元测试。
---
## 5. 风险与回退
| 风险 | 对策 |
|---|---|
| `FramePtr``std::shared_ptr`C API 中需要管理引用计数 | `OliveFrame` 不透明指针内部持有 `std::shared_ptr`,销毁时自动减引用计数。若需要延长生命周期,可新增 `olive_frame_ref/unref`。 |
| `Decoder::Open` 是异步/多线程的 | C API 层面先做同步封装(等待 Open 完成)。若性能不满足,后续可新增异步回调接口。 |
| `common/` 中的 `xmlutils.h` 依赖 Qt XML | 这是允许的(Qt::Core 的一部分),但需注意 `common/` 中若混入 GUI 相关代码(如 `QMessageBox`),必须移出。 |
| FFmpeg 的 `AVFrame``olive::Frame` 转换在 C API 边界 | 保持内部实现不变,C API 只操作 `olive::Frame`。 |
@@ -1,305 +0,0 @@
# liboliveplugin.so — OFX 插件宿主支持
> **依赖**`libolivecore.so`
> **外部依赖**`third_party/openfx/HostSupport``OfxHost` 静态库),expatQt::Core
> **包含源码**`app/pluginSupport/`
> **当前状态**:单体 OBJECT 库的一部分,通过 `target_link_libraries(olive-editor PUBLIC OfxHost)` 隐式链接
> **改造难度**:⭐⭐(较简单,接口相对独立)
---
## 1. 当前状态分析
`app/pluginSupport/` 实现 OFXOpenFX)标准的 Host 端接口,使 Olive 能够加载第三方插件(如 Sapphire、Neat Video 等)。
| 组件 | 说明 |
|---|---|
| `OliveHost` | OFX Host 接口主实现 |
| `PluginInstance` | 单个插件实例管理 |
| `OliveClip` / `OliveClipInstance` | OFX Clip 接口封装 |
| `OliveParam` / `OliveParamInstance` | OFX 参数接口封装 |
| `node/plugins/PluginNode` | OFX 插件在节点图中的封装节点 |
**特点**
- `pluginSupport/``node/plugins/PluginNode` 存在双向依赖。
- OFX Host 支持库(`third_party/openfx/HostSupport`)是第三方代码,不应修改其接口。
- 插件渲染需要 OpenGL 上下文,因此 `liboliveplugin.so` 需要与渲染层协作。
**决策**:由于 `PluginNode` 继承自 `Node`(在 `libolivenode.so` 中),`PluginNode` 应留在 `libolivenode.so` 中。`liboliveplugin.so` 只包含纯 Host 支持代码(`pluginSupport/`),通过 C API 向 `libolivenode.so` 暴露插件加载和管理能力。
---
## 2. C API 设计
### 2.1 头文件:`c_api/include/olive/plugin_api.h`
```c
#ifndef OLIVE_PLUGIN_API_H
#define OLIVE_PLUGIN_API_H
#ifdef __cplusplus
extern "C" {
#endif
#include "core_api.h"
#define OLIVE_PLUGIN_API_VERSION 1
#ifdef OLIVE_BUILDING_PLUGIN
# define OLIVE_PLUGIN_API __attribute__((visibility("default")))
#else
# define OLIVE_PLUGIN_API
#endif
/* ========== 类型前向声明 ========== */
typedef struct OlivePluginHost OlivePluginHost;
typedef struct OlivePlugin OlivePlugin;
typedef struct OlivePluginInstance OlivePluginInstance;
typedef struct OlivePluginParam OlivePluginParam;
/* ========== API 版本 ========== */
OLIVE_PLUGIN_API int olive_plugin_api_version(void);
/* ========== Host 生命周期 ========== */
OLIVE_PLUGIN_API OlivePluginHost* olive_plugin_host_create(void);
OLIVE_PLUGIN_API void olive_plugin_host_destroy(OlivePluginHost* host);
// 设置插件搜索路径(可多次调用添加多个路径)
OLIVE_PLUGIN_API int olive_plugin_host_add_path(OlivePluginHost* host, const char* path);
// 扫描所有路径,加载可用插件
OLIVE_PLUGIN_API int olive_plugin_host_rescan(OlivePluginHost* host);
// 获取已加载插件数量
OLIVE_PLUGIN_API int olive_plugin_host_plugin_count(OlivePluginHost* host);
// 获取指定索引的插件
OLIVE_PLUGIN_API OlivePlugin* olive_plugin_host_get_plugin(OlivePluginHost* host, int index);
/* ========== Plugin 信息 ========== */
OLIVE_PLUGIN_API const char* olive_plugin_get_id(OlivePlugin* plugin);
OLIVE_PLUGIN_API const char* olive_plugin_get_name(OlivePlugin* plugin);
OLIVE_PLUGIN_API const char* olive_plugin_get_group(OlivePlugin* plugin); // 分类,如 "Filter/Blur"
OLIVE_PLUGIN_API int olive_plugin_is_hardware_rendering_supported(OlivePlugin* plugin);
/* ========== Plugin Instance ========== */
OLIVE_PLUGIN_API OlivePluginInstance* olive_plugin_instance_create(OlivePlugin* plugin,
int width,
int height);
OLIVE_PLUGIN_API void olive_plugin_instance_destroy(OlivePluginInstance* instance);
// 参数操作(通过字符串名称)
OLIVE_PLUGIN_API int olive_plugin_instance_set_param_int(OlivePluginInstance* instance,
const char* param_name,
int value);
OLIVE_PLUGIN_API int olive_plugin_instance_set_param_double(OlivePluginInstance* instance,
const char* param_name,
double value);
OLIVE_PLUGIN_API int olive_plugin_instance_set_param_string(OlivePluginInstance* instance,
const char* param_name,
const char* value);
// 渲染一帧(输入/输出均为 Frame)
OLIVE_PLUGIN_API int olive_plugin_instance_render(OlivePluginInstance* instance,
OliveRational time,
OliveFrame* input_frame,
OliveFrame** output_frame);
/* ========== Param 枚举(用于 UI 构建控件) ========== */
OLIVE_PLUGIN_API int olive_plugin_instance_param_count(OlivePluginInstance* instance);
OLIVE_PLUGIN_API OlivePluginParam* olive_plugin_instance_get_param(OlivePluginInstance* instance,
int index);
OLIVE_PLUGIN_API const char* olive_plugin_param_get_name(OlivePluginParam* param);
OLIVE_PLUGIN_API const char* olive_plugin_param_get_label(OlivePluginParam* param);
OLIVE_PLUGIN_API int olive_plugin_param_get_type(OlivePluginParam* param); // 0=int, 1=double, 2=string, 3=bool, 4=color, 5=choice
OLIVE_PLUGIN_API int olive_plugin_param_get_int_min(OlivePluginParam* param);
OLIVE_PLUGIN_API int olive_plugin_param_get_int_max(OlivePluginParam* param);
OLIVE_PLUGIN_API double olive_plugin_param_get_double_min(OlivePluginParam* param);
OLIVE_PLUGIN_API double olive_plugin_param_get_double_max(OlivePluginParam* param);
OLIVE_PLUGIN_API int olive_plugin_param_get_choice_count(OlivePluginParam* param);
OLIVE_PLUGIN_API const char* olive_plugin_param_get_choice_label(OlivePluginParam* param, int index);
#ifdef __cplusplus
}
#endif
#endif // OLIVE_PLUGIN_API_H
```
### 2.2 实现要点
```cpp
// c_api/src/plugin_api.cpp
#include "olive/plugin_api.h"
#include "pluginSupport/olivehost.h"
#include "pluginSupport/plugininstance.h"
#include "pluginSupport/oliveparam.h"
#include "codec/frame.h"
struct OlivePluginHost {
olive::OliveHost* impl;
};
struct OlivePlugin {
olive::Plugin* impl; // 内部插件描述对象
};
struct OlivePluginInstance {
olive::PluginInstance* impl;
};
// ...
extern "C" {
OlivePluginHost* olive_plugin_host_create(void) {
try {
auto* h = new OlivePluginHost();
h->impl = new olive::OliveHost();
return h;
} catch (...) {
return nullptr;
}
}
void olive_plugin_host_destroy(OlivePluginHost* host) {
if (host) {
delete host->impl;
delete host;
}
}
int olive_plugin_host_add_path(OlivePluginHost* host, const char* path) {
if (!host || !path) return OLIVE_ERROR_INVALID;
try {
host->impl->AddPath(QString::fromUtf8(path));
return OLIVE_OK;
} catch (...) {
return OLIVE_ERROR_GENERIC;
}
}
int olive_plugin_host_rescan(OlivePluginHost* host) {
if (!host) return OLIVE_ERROR_INVALID;
try {
host->impl->RescanPlugins();
return OLIVE_OK;
} catch (...) {
return OLIVE_ERROR_GENERIC;
}
}
// ... 其他封装类似 ...
} // extern "C"
```
---
## 3. CMake 改造
### 3.1 `app/pluginSupport/CMakeLists.txt`
```cmake
set(PLUGIN_INTERNAL_SOURCES
olivehost.cpp olivehost.h
plugininstance.cpp plugininstance.h
oliveclip.cpp oliveclip.h
oliveclipinstance.cpp oliveclipinstance.h
oliveparam.cpp oliveparam.h
oliveparaminstance.cpp oliveparaminstance.h
# ...
)
set(PLUGIN_API_SOURCES
${CMAKE_SOURCE_DIR}/c_api/src/plugin_api.cpp
)
add_library(oliveplugin SHARED
${PLUGIN_INTERNAL_SOURCES}
${PLUGIN_API_SOURCES}
)
target_compile_definitions(oliveplugin PRIVATE OLIVE_BUILDING_PLUGIN)
target_include_directories(oliveplugin
PRIVATE
${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/third_party/openfx/include
${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include
${CMAKE_SOURCE_DIR}/c_api/include
PUBLIC
$<INSTALL_INTERFACE:include>
)
target_link_libraries(oliveplugin
PUBLIC
olivecore
OfxHost # third_party/openfx/HostSupport 构建的目标
EXPAT::EXPAT
Qt${QT_VERSION_MAJOR}::Core
)
set_target_properties(oliveplugin PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
install(TARGETS oliveplugin DESTINATION lib)
install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/plugin_api.h DESTINATION include/olive)
```
---
## 4. 小步快跑实施步骤
### Step 0: 隔离 PluginNode1 天)
- [ ]`app/node/plugins/PluginNode` 移动到 `app/node/` 下(或保持原位,但确保它编译进 `libolivenode.so` 而非 `liboliveplugin.so`)。
- [ ] 确认 `PluginNode``pluginSupport/` 的依赖方向:PluginNode 使用 pluginSupport 的类,而非相反。
**验收标准**`liboliveplugin.so` 编译时不包含任何 `node/` 下的源文件。
### Step 1: 构建 liboliveplugin.so1 天)
- [ ] 创建 `app/pluginSupport/CMakeLists.txt`(若尚无)。
- [ ]`pluginSupport/` 的源文件从主 OBJECT 库中移出,单独构建为 `oliveplugin SHARED`
- [ ] 确保 `OfxHost` 静态库先被构建(`third_party/openfx/HostSupport`)。
**验收标准**`liboliveplugin.so` 编译成功,能通过 `dlsym` 找到 `olive_plugin_api_version`
### Step 2: 最小 C API2 天)
- [ ] 先实现最必需的接口:
- `olive_plugin_host_create/destroy/add_path/rescan`
- `olive_plugin_host_plugin_count/get_plugin`
- `olive_plugin_get_id/name`
- [ ] 暂不实现:渲染接口(`olive_plugin_instance_render`)、参数枚举。
**验收标准**:主进程可以扫描 OFX 插件目录并列出所有插件名称。
### Step 3: 扩展渲染接口(2 天)
- [ ] 实现 `olive_plugin_instance_create/destroy`
- [ ] 实现 `olive_plugin_instance_render`(输入输出 `OliveFrame*`)。
- [ ] 此步骤需要 `libolivecodec.so``OliveFrame` 定义已就绪。
**验收标准**:可以创建一个 OFX 插件实例,传入一帧,获取处理后的一帧。
### Step 4: 参数枚举(2 天)
- [ ] 实现参数枚举接口,使 UI 层可以通过 C API 自动构建参数控件。
- [ ] 编写测试:加载一个已知插件(如 OFX 示例插件),验证参数数量与类型正确。
---
## 5. 风险与回退
| 风险 | 对策 |
|---|---|
| `OfxHost` 静态库中的符号与动态库导出冲突 | `OfxHost` 保持静态链接进 `liboliveplugin.so`,其符号不对外导出(`hidden` 可见性)。 |
| OFX 插件需要 OpenGL 上下文 | 渲染接口 `olive_plugin_instance_render` 需要传入或绑定 GL 上下文。在"用完即弃"的渲染子进程模型中,这天然解决:子进程自己创建 GL 上下文,插件在其上渲染。 |
| `PluginNode` 需要 `PluginInstance` 的 C++ 类 | `PluginNode``libolivenode.so` 内部,可以直接包含 `pluginSupport/` 的 C++ 头文件(因为 node 库可以在编译时访问 pluginSupport 源码)。只有跨库边界才需要 C API。 |
| OFX 插件多实例状态管理复杂 | C API 中每个 `OlivePluginInstance*` 对应一个独立的 OFX 实例句柄,状态完全隔离。 |
@@ -1,267 +0,0 @@
# liboliveaudio.so — 音频播放与处理
> **依赖**`libolivecore.so`
> **外部依赖**PortAudioQt::Core
> **包含源码**`app/audio/`
> **当前状态**:单体 OBJECT 库的一部分
> **改造难度**:⭐⭐(较简单)
---
## 1. 当前状态分析
`app/audio/` 负责音频播放管理、音频处理管线和波形可视化数据。
| 组件 | 说明 |
|---|---|
| `audiomanager.h/cpp` | 音频播放管理器(单例),基于 PortAudio |
| `audioprocessor.h/cpp` | 音频处理管线 |
| `audiovisualwaveform.h/cpp` | 音频波形数据(用于 UI 显示) |
| `audiohybriddevice.h/cpp` | 音频混合设备 |
**特点**
- 相对独立,不直接依赖 `node/``render/`(通过回调或数据缓冲区交互)。
- `AudioManager` 是单例,C API 中需要妥善处理单例的生命周期。
- 音频数据量较小,实时性要求高。
---
## 2. C API 设计
### 2.1 头文件:`c_api/include/olive/audio_api.h`
```c
#ifndef OLIVE_AUDIO_API_H
#define OLIVE_AUDIO_API_H
#ifdef __cplusplus
extern "C" {
#endif
#include "core_api.h"
#define OLIVE_AUDIO_API_VERSION 1
#ifdef OLIVE_BUILDING_AUDIO
# define OLIVE_AUDIO_API __attribute__((visibility("default")))
#else
# define OLIVE_AUDIO_API
#endif
/* ========== 类型前向声明 ========== */
typedef struct OliveAudioManager OliveAudioManager;
typedef struct OliveAudioProcessor OliveAudioProcessor;
typedef struct OliveAudioWaveform OliveAudioWaveform;
/* ========== API 版本 ========== */
OLIVE_AUDIO_API int olive_audio_api_version(void);
/* ========== AudioManager(播放控制) ========== */
OLIVE_AUDIO_API OliveAudioManager* olive_audio_manager_get_instance(void);
OLIVE_AUDIO_API void olive_audio_manager_release_instance(OliveAudioManager* mgr);
OLIVE_AUDIO_API int olive_audio_manager_init(OliveAudioManager* mgr, OliveAudioParams params);
OLIVE_AUDIO_API void olive_audio_manager_shutdown(OliveAudioManager* mgr);
// 播放控制
OLIVE_AUDIO_API int olive_audio_manager_play(OliveAudioManager* mgr);
OLIVE_AUDIO_API int olive_audio_manager_pause(OliveAudioManager* mgr);
OLIVE_AUDIO_API int olive_audio_manager_stop(OliveAudioManager* mgr);
OLIVE_AUDIO_API int olive_audio_manager_is_playing(OliveAudioManager* mgr);
// 推入待播放的音频缓冲区(主进程渲染后推入)
OLIVE_AUDIO_API int olive_audio_manager_push_buffer(OliveAudioManager* mgr,
OliveSampleBuffer* buffer);
// 获取当前播放时间
OLIVE_AUDIO_API OliveRational olive_audio_manager_get_playback_time(OliveAudioManager* mgr);
/* ========== AudioProcessor(处理管线) ========== */
OLIVE_AUDIO_API OliveAudioProcessor* olive_audio_processor_create(OliveAudioParams params);
OLIVE_AUDIO_API void olive_audio_processor_destroy(OliveAudioProcessor* proc);
// 处理一帧音频(应用音量、声像等)
OLIVE_AUDIO_API int olive_audio_processor_process(OliveAudioProcessor* proc,
OliveSampleBuffer* input,
OliveSampleBuffer** output);
/* ========== AudioWaveform(波形数据) ========== */
OLIVE_AUDIO_API OliveAudioWaveform* olive_audio_waveform_create(OliveAudioParams params,
OliveRational duration);
OLIVE_AUDIO_API void olive_audio_waveform_destroy(OliveAudioWaveform* wf);
// 从采样缓冲区生成波形数据
OLIVE_AUDIO_API int olive_audio_waveform_generate(OliveAudioWaveform* wf,
OliveSampleBuffer* buffer,
OliveRational start_time);
// 获取指定时间点的波形峰值(用于 UI 绘制)
OLIVE_AUDIO_API float olive_audio_waveform_get_peak(OliveAudioWaveform* wf,
OliveRational time,
int channel);
// 获取波形数据数组(用于批量绘制)
OLIVE_AUDIO_API const float* olive_audio_waveform_get_peaks(OliveAudioWaveform* wf,
int channel,
int* out_count);
#ifdef __cplusplus
}
#endif
#endif // OLIVE_AUDIO_API_H
```
### 2.2 实现要点
```cpp
// c_api/src/audio_api.cpp
#include "olive/audio_api.h"
#include "audio/audiomanager.h"
#include "audio/audioprocessor.h"
#include "audio/audiovisualwaveform.h"
struct OliveAudioManager {
// AudioManager 是单例,此处不持有所有权,只作为句柄
olive::AudioManager* impl;
};
extern "C" {
OliveAudioManager* olive_audio_manager_get_instance(void) {
static OliveAudioManager mgr;
mgr.impl = olive::AudioManager::instance();
return &mgr;
}
void olive_audio_manager_release_instance(OliveAudioManager* mgr) {
// 单例不在这里销毁
(void)mgr;
}
int olive_audio_manager_init(OliveAudioManager* mgr, OliveAudioParams params) {
if (!mgr || !mgr->impl) return OLIVE_ERROR_INVALID;
try {
olive::AudioParams cpp_params;
cpp_params.set_sample_rate(params.sample_rate);
cpp_params.set_channel_layout(params.channel_layout);
// ... 转换 format ...
mgr->impl->SetParameters(cpp_params);
return OLIVE_OK;
} catch (...) {
return OLIVE_ERROR_GENERIC;
}
}
int olive_audio_manager_push_buffer(OliveAudioManager* mgr, OliveSampleBuffer* buffer) {
if (!mgr || !mgr->impl || !buffer) return OLIVE_ERROR_INVALID;
try {
mgr->impl->PushBuffer(buffer->impl);
return OLIVE_OK;
} catch (...) {
return OLIVE_ERROR_GENERIC;
}
}
// ... 其他函数类似封装 ...
} // extern "C"
```
---
## 3. CMake 改造
```cmake
# app/audio/CMakeLists.txt
set(AUDIO_INTERNAL_SOURCES
audiomanager.cpp audiomanager.h
audioprocessor.cpp audioprocessor.h
audiovisualwaveform.cpp audiovisualwaveform.h
audiohybriddevice.cpp audiohybriddevice.h
)
set(AUDIO_API_SOURCES
${CMAKE_SOURCE_DIR}/c_api/src/audio_api.cpp
)
add_library(oliveaudio SHARED
${AUDIO_INTERNAL_SOURCES}
${AUDIO_API_SOURCES}
)
target_compile_definitions(oliveaudio PRIVATE OLIVE_BUILDING_AUDIO)
target_include_directories(oliveaudio
PRIVATE
${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/c_api/include
PUBLIC
$<INSTALL_INTERFACE:include>
)
target_link_libraries(oliveaudio
PUBLIC
olivecore
${PORTAUDIO_LIBRARIES}
Qt${QT_VERSION_MAJOR}::Core
)
set_target_properties(oliveaudio PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
install(TARGETS oliveaudio DESTINATION lib)
install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/audio_api.h DESTINATION include/olive)
```
---
## 4. 小步快跑实施步骤
### Step 0: 分析音频数据流(半天)
- [ ] 梳理 `AudioManager` 的数据流:谁调用 `PushBuffer`?谁消费?
- [ ] 确认 `audio/``node/` 的依赖情况(`AudioProcessor` 是否直接操作 `Node`?)。
**验收标准**:确认 `audio/` 可以独立于 `node/` 编译(可能只需要 `SampleBuffer``AudioParams` 类型)。
### Step 1: 独立编译 liboliveaudio.so1 天)
- [ ]`app/audio/` 从主 OBJECT 库移出,单独构建为 `oliveaudio SHARED`
- [ ] 确保 `AudioManager` 单例的初始化顺序正确(Qt 的 `Q_GLOBAL_STATIC` 或延迟初始化)。
**验收标准**`liboliveaudio.so` 编译成功。
### Step 2: 最小 C API1 天)
- [ ] 实现播放控制:`init`, `play`, `pause`, `stop`
- [ ] 实现 `push_buffer`(关键接口,主进程渲染音频后推入播放队列)。
**验收标准**:可以通过 C API 初始化音频、播放一段静音缓冲区。
### Step 3: 波形数据接口(1 天)
- [ ] 实现 `olive_audio_waveform_create/generate/get_peak`
- [ ] 此接口供 UI 层调用以绘制音频波形。
**验收标准**:给定一个 `OliveSampleBuffer*`,可以生成波形并查询任意时间点的峰值。
### Step 4: 处理器接口(1 天)
- [ ] 实现 `olive_audio_processor_create/process`
- [ ] 用于节点图中的音频处理链(音量、声像)。
---
## 5. 风险与回退
| 风险 | 对策 |
|---|---|
| `AudioManager` 单例在动态库卸载后仍被引用 | 主进程退出前先停止播放并释放 `AudioManager`,再卸载动态库。 |
| PortAudio 回调线程与主线程的交互 | C API 中 `push_buffer` 是线程安全的(内部用 `QMutex` 保护队列),C API 调用者无需额外同步。 |
| 实时音频延迟要求 | C API 不增加额外拷贝:`push_buffer` 内部直接传递 `SampleBuffer` 的共享指针。 |
| 音频处理需要节点图信息 | `AudioProcessor` 的参数(如音量值)通过 C API 直接设置,不涉及节点图遍历。节点图到音频参数的映射在 `libolivenode.so` 中完成。 |
-413
View File
@@ -1,413 +0,0 @@
# libolivenode.so — 节点图系统
> **依赖**`libolivecore.so`, `libolivecodec.so`
> **外部依赖**Qt::CoreQObject, QString, XML
> **包含源码**`app/node/`, `app/timeline/`, `app/undo/`, `app/config/`
> **当前状态**:整个项目的**核心枢纽**,被 `render/`, `widget/`, `panel/`, `task/` 等几乎所有上层模块依赖
> **改造难度**:⭐⭐⭐⭐⭐(最困难,耦合最深)
---
## 1. 当前状态分析
`app/node/` 是整个编辑器的**数据与计算模型核心**,采用节点图范式组织所有处理。其特点是:
1. **被几乎所有模块依赖**`render/` 遍历节点图,`widget/` 绘制节点连接,`panel/` 包装节点编辑器,`task/` 在导出时读取节点图。
2. **头文件耦合严重**`Node.h` 直接 `#include``codec/frame.h``render/` 下的多个头文件(缓存类型、作业类型等)。
3. **Qt 深度集成**`Node` 继承 `QObject`,使用信号槽、元对象系统、`QVariant`
4. **序列化内建**`ProjectSerializer` 支持将节点图保存/加载为 XML。
### 1.1 关键耦合点与解耦策略
| 耦合点 | 当前状态 | 解耦策略 |
|---|---|---|
| `Node.h` 包含 `render/rendercache.h` | `Node` 直接操作 `FrameHashCache` | 将缓存失效抽象为虚函数 `InvalidateCache()`,或注入 `NodeCacheCallbacks` 接口指针。移除 `rendercache.h` 的包含。 |
| `Node.h` 包含 `render/job/*.h` | `Node::ProcessShader()` 等虚函数使用具体 Job 类型 | 将 `ProcessShader` 等改为接受 `const void* job_data` + `JobType` 枚举,内部再 `static_cast`。或前向声明 Job 类(若已是不透明指针)。 |
| `Footage``node/project/footage/`)依赖 `Decoder` | `Footage` 需要解码器信息预览 | 保留此依赖,`libolivenode.so` 链接 `libolivecodec.so` 是合理的。 |
| `ViewerOutput` 被 UI 直接引用 | `ViewerOutput` 是节点图与 UI 的桥梁 | `ViewerOutput` 保留在 `node/` 中,C API 暴露 `OliveViewerOutput*` 句柄。 |
| `timeline/` 依赖 `node/` | `TimelineMarker` 等引用 `Node` | `timeline/` 并入 `libolivenode.so`,不单独拆分。 |
| `undo/` 依赖 `node/` | `UndoCommand` 操作 `Node` 对象 | `undo/` 并入 `libolivenode.so`。 |
---
## 2. C API 设计
### 2.1 头文件:`c_api/include/olive/node_api.h`
```c
#ifndef OLIVE_NODE_API_H
#define OLIVE_NODE_API_H
#ifdef __cplusplus
extern "C" {
#endif
#include "core_api.h"
#define OLIVE_NODE_API_VERSION 1
#ifdef OLIVE_BUILDING_NODE
# define OLIVE_NODE_API __attribute__((visibility("default")))
#else
# define OLIVE_NODE_API
#endif
/* ========== 不透明类型 ========== */
typedef struct OliveNodeGraph OliveNodeGraph;
typedef struct OliveNode OliveNode;
typedef struct OliveNodeInput OliveNodeInput;
typedef struct OliveNodeOutput OliveNodeOutput;
typedef struct OliveParam OliveParam;
typedef struct OliveKeyframe OliveKeyframe;
typedef struct OliveProject OliveProject;
typedef struct OliveSequence OliveSequence;
typedef struct OliveTrack OliveTrack;
typedef struct OliveClip OliveClip;
typedef struct OliveViewerOutput OliveViewerOutput;
/* ========== 枚举 ========== */
typedef enum {
OLIVE_NODE_TYPE_UNKNOWN = 0,
OLIVE_NODE_TYPE_INPUT,
OLIVE_NODE_TYPE_OUTPUT,
OLIVE_NODE_TYPE_FILTER,
OLIVE_NODE_TYPE_DISTORT,
OLIVE_NODE_TYPE_GENERATOR,
OLIVE_NODE_TYPE_COLOR,
OLIVE_NODE_TYPE_AUDIO,
OLIVE_NODE_TYPE_TRANSITION,
OLIVE_NODE_TYPE_PLUGIN,
OLIVE_NODE_TYPE_GROUP,
} OliveNodeType;
typedef enum {
OLIVE_PARAM_TYPE_INT = 0,
OLIVE_PARAM_TYPE_DOUBLE,
OLIVE_PARAM_TYPE_STRING,
OLIVE_PARAM_TYPE_RATIONAL,
OLIVE_PARAM_TYPE_COLOR,
OLIVE_PARAM_TYPE_BOOL,
OLIVE_PARAM_TYPE_VECTOR2,
OLIVE_PARAM_TYPE_VECTOR3,
OLIVE_PARAM_TYPE_VECTOR4,
} OliveParamType;
/* ========== API 版本 ========== */
OLIVE_NODE_API int olive_node_api_version(void);
/* ========== NodeGraph ========== */
OLIVE_NODE_API OliveNodeGraph* olive_node_graph_create(void);
OLIVE_NODE_API void olive_node_graph_destroy(OliveNodeGraph* g);
// 序列化
OLIVE_NODE_API int olive_node_graph_load_xml(OliveNodeGraph* g,
const char* xml_data,
size_t xml_len);
OLIVE_NODE_API char* olive_node_graph_save_xml(OliveNodeGraph* g,
size_t* out_len);
// 节点增删查
OLIVE_NODE_API OliveNode* olive_node_graph_add_node(OliveNodeGraph* g,
const char* node_type_id,
const char* node_id);
OLIVE_NODE_API int olive_node_graph_remove_node(OliveNodeGraph* g, OliveNode* node);
OLIVE_NODE_API OliveNode* olive_node_graph_find_node(OliveNodeGraph* g,
const char* node_id);
OLIVE_NODE_API int olive_node_graph_node_count(OliveNodeGraph* g);
OLIVE_NODE_API OliveNode* olive_node_graph_get_node(OliveNodeGraph* g, int index);
// 连接管理
OLIVE_NODE_API int olive_node_connect(OliveNode* from_node,
int output_index,
OliveNode* to_node,
int input_index);
OLIVE_NODE_API int olive_node_disconnect(OliveNode* node, int input_index);
OLIVE_NODE_API OliveNode* olive_node_get_connected_node(OliveNode* node,
int input_index);
/* ========== Node 属性 ========== */
OLIVE_NODE_API const char* olive_node_get_id(OliveNode* node);
OLIVE_NODE_API const char* olive_node_get_label(OliveNode* node);
OLIVE_NODE_API OliveNodeType olive_node_get_type(OliveNode* node);
OLIVE_NODE_API const char* olive_node_get_type_id(OliveNode* node);
OLIVE_NODE_API int olive_node_input_count(OliveNode* node);
OLIVE_NODE_API int olive_node_output_count(OliveNode* node);
/* ========== Param 操作 ========== */
OLIVE_NODE_API int olive_node_param_count(OliveNode* node);
OLIVE_NODE_API OliveParam* olive_node_get_param(OliveNode* node, int index);
OLIVE_NODE_API OliveParam* olive_node_find_param(OliveNode* node,
const char* param_name);
OLIVE_NODE_API const char* olive_param_get_name(OliveParam* param);
OLIVE_NODE_API OliveParamType olive_param_get_type(OliveParam* param);
OLIVE_NODE_API int olive_param_set_int(OliveParam* param, int64_t value);
OLIVE_NODE_API int olive_param_set_double(OliveParam* param, double value);
OLIVE_NODE_API int olive_param_set_rational(OliveParam* param, OliveRational value);
OLIVE_NODE_API int olive_param_set_color(OliveParam* param, OliveColor value);
OLIVE_NODE_API int olive_param_set_string(OliveParam* param, const char* value);
OLIVE_NODE_API int64_t olive_param_get_int(OliveParam* param);
OLIVE_NODE_API double olive_param_get_double(OliveParam* param);
OLIVE_NODE_API OliveRational olive_param_get_rational(OliveParam* param);
OLIVE_NODE_API OliveColor olive_param_get_color(OliveParam* param);
/* ========== Keyframe ========== */
OLIVE_NODE_API int olive_param_add_keyframe(OliveParam* param,
OliveRational time,
double value);
OLIVE_NODE_API int olive_param_remove_keyframe(OliveParam* param,
OliveRational time);
OLIVE_NODE_API int olive_param_keyframe_count(OliveParam* param);
/* ========== Project ========== */
OLIVE_NODE_API OliveProject* olive_project_create(const char* name);
OLIVE_NODE_API void olive_project_destroy(OliveProject* proj);
OLIVE_NODE_API int olive_project_load_file(OliveProject* proj, const char* filename);
OLIVE_NODE_API int olive_project_save_file(OliveProject* proj, const char* filename);
OLIVE_NODE_API OliveNodeGraph* olive_project_get_graph(OliveProject* proj);
/* ========== Sequence / Timeline ========== */
OLIVE_NODE_API OliveSequence* olive_sequence_create(const char* name,
OliveVideoParams vparams,
OliveAudioParams aparams);
OLIVE_NODE_API OliveViewerOutput* olive_sequence_get_viewer_output(OliveSequence* seq);
/* ========== ViewerOutput(渲染目标) ========== */
OLIVE_NODE_API const char* olive_viewer_output_get_node_id(OliveViewerOutput* viewer);
OLIVE_NODE_API OliveVideoParams olive_viewer_output_get_video_params(OliveViewerOutput* viewer);
OLIVE_NODE_API OliveAudioParams olive_viewer_output_get_audio_params(OliveViewerOutput* viewer);
/* ========== Undo ========== */
typedef struct OliveUndoStack OliveUndoStack;
OLIVE_NODE_API OliveUndoStack* olive_undo_stack_create(void);
OLIVE_NODE_API void olive_undo_stack_destroy(OliveUndoStack* stack);
OLIVE_NODE_API void olive_undo_stack_push(OliveUndoStack* stack,
const char* action_name,
void* undo_data,
void (*undo_fn)(void*),
void (*redo_fn)(void*),
void (*free_fn)(void*));
OLIVE_NODE_API int olive_undo_stack_can_undo(OliveUndoStack* stack);
OLIVE_NODE_API int olive_undo_stack_can_redo(OliveUndoStack* stack);
OLIVE_NODE_API void olive_undo_stack_undo(OliveUndoStack* stack);
OLIVE_NODE_API void olive_undo_stack_redo(OliveUndoStack* stack);
OLIVE_NODE_API void olive_undo_stack_clear(OliveUndoStack* stack);
#ifdef __cplusplus
}
#endif
#endif // OLIVE_NODE_API_H
```
### 2.2 关键解耦实现:NodeCacheCallbacks
```cpp
// app/node/node.h(改造后,移除 render/ 头文件包含)
// 前向声明
class NodeCacheCallbacks;
class Node : public QObject {
// ...
void SetCacheCallbacks(NodeCacheCallbacks* callbacks);
protected:
virtual void InvalidateCacheInternal(const TimeRange& range);
private:
NodeCacheCallbacks* cache_callbacks_ = nullptr;
};
// app/node/nodecachecallbacks.h(新增)
class NodeCacheCallbacks {
public:
virtual ~NodeCacheCallbacks() = default;
virtual void InvalidateCache(const QString& cache_id, const TimeRange& range) = 0;
virtual void InvalidateAllCaches() = 0;
};
```
`RenderManager`(在 `liboliverender.so` 中)实现 `NodeCacheCallbacks`,并在创建节点时注入:
```cpp
class RenderCacheCallbacks : public NodeCacheCallbacks {
void InvalidateCache(const QString& cache_id, const TimeRange& range) override {
// 原有 FrameHashCache 的失效逻辑
}
// ...
};
```
这样 `Node.h` 不再需要包含 `render/rendercache.h`,编译期依赖被打破。
### 2.3 关键解耦实现:RenderJob 虚函数参数抽象
当前 `Node` 有虚函数:
```cpp
// 改造前
virtual void ProcessShader(TexturePtr destination, const Node* node, const ShaderJob* job);
```
改造后:
```cpp
// app/node/jobtypes.h(新增,只含枚举和基类,无 render/ 依赖)
enum class NodeJobType {
kShader,
kGenerate,
kFootage,
kColorTransform,
kSample,
kCache,
};
struct NodeJobData {
NodeJobType type;
void* data; // 实际数据由 render/ 中的具体类解释
};
// app/node/node.h
virtual void ProcessJob(TexturePtr destination, const NodeJobData& job);
```
`RenderProcessor`(在 `liboliverender.so` 中)调用时:
```cpp
ShaderJob job = ...;
NodeJobData data{NodeJobType::kShader, &job};
node->ProcessJob(destination, data);
```
---
## 3. CMake 改造
```cmake
# app/node/CMakeLists.txt
set(NODE_INTERNAL_SOURCES
node.cpp node.h
traverser.cpp traverser.h
traverserproxy.cpp traverserproxy.h
nodevalue.cpp nodevalue.h
# ... 所有 node/ 子目录源文件
)
set(TIMELINE_SOURCES
../timeline/timelinecoordinate.cpp ../timeline/timelinecoordinate.h
../timeline/timelinemarker.cpp ../timeline/timelinemarker.h
../timeline/timelineworkarea.cpp ../timeline/timelineworkarea.h
../timeline/undo/*.cpp ../timeline/undo/*.h
)
set(UNDO_SOURCES
../undo/undocommand.cpp ../undo/undocommand.h
../undo/undostack.cpp ../undo/undostack.h
)
set(CONFIG_SOURCES
../config/config.cpp ../config/config.h
)
set(NODE_API_SOURCES
${CMAKE_SOURCE_DIR}/c_api/src/node_api.cpp
)
add_library(olivenode SHARED
${NODE_INTERNAL_SOURCES}
${TIMELINE_SOURCES}
${UNDO_SOURCES}
${CONFIG_SOURCES}
${NODE_API_SOURCES}
)
target_compile_definitions(olivenode PRIVATE OLIVE_BUILDING_NODE)
target_include_directories(olivenode
PRIVATE
${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/c_api/include
PUBLIC
$<INSTALL_INTERFACE:include>
)
target_link_libraries(olivenode
PUBLIC
olivecore
olivecodec
Qt${QT_VERSION_MAJOR}::Core
)
set_target_properties(olivenode PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
install(TARGETS olivenode DESTINATION lib)
install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/node_api.h DESTINATION include/olive)
```
---
## 4. 小步快跑实施步骤
### Step 0: 头文件解耦(3–4 天,最关键)
- [ ] 创建 `app/node/nodecachecallbacks.h`,定义 `NodeCacheCallbacks` 接口。
- [ ] 修改 `Node.h`:移除 `render/rendercache.h` 包含,添加 `NodeCacheCallbacks*` 成员和 `SetCacheCallbacks()` 方法。
- [ ] 创建 `app/node/jobtypes.h`,定义 `NodeJobType` 枚举和 `NodeJobData` 结构体。
- [ ] 修改 `Node.h`:将所有 `ProcessXxx` 虚函数合并为 `ProcessJob(TexturePtr, const NodeJobData&)`,或保留原签名但将参数类型改为前向声明。
- [ ] 修改 `RenderProcessor`:适配新的 `NodeCacheCallbacks``NodeJobData`
**验收标准**`app/node/` 目录可以独立编译,不直接或间接包含 `app/render/` 下的任何头文件。
### Step 1: 独立编译 libolivenode.so1 天)
- [ ]`node/`, `timeline/`, `undo/`, `config/` 的源文件聚合,构建为 `olivenode SHARED`
- [ ] 处理 `node/` 下的 `add_subdirectory` 嵌套,确保所有源文件被正确收集。
**验收标准**`libolivenode.so` 编译成功,`nm -D libolivenode.so | grep olive_node` 能看到导出的 C 符号。
### Step 2: 最小 C API2 天)
- [ ] 先实现项目级接口:
- `olive_project_create/destroy/load_file/save_file`
- `olive_node_graph_create/destroy/load_xml/save_xml`
- [ ] 这些接口是渲染子进程最需要的:子进程需要加载 XML 节点图并渲染。
**验收标准**:可以用 C API 创建一个项目、保存为 XML、再加载回来,内容一致。
### Step 3: 节点操作 API2 天)
- [ ] 实现节点增删查改:`add_node`, `remove_node`, `find_node`, `connect`, `disconnect`
- [ ] 实现参数读写:`set_param_double`, `get_param_double`, `set_param_rational` 等。
**验收标准**:可以用 C API 构建一个简单的节点图(如 Generator -> ViewerOutput),并序列化为 XML。
### Step 4: Undo API1 天)
- [ ] 实现 `olive_undo_stack_*` 系列函数。
- [ ] C API 的 undo 采用函数指针回调模式,避免暴露 C++ 的 `UndoCommand` 类。
### Step 5: ViewerOutput 和 Sequence1 天)
- [ ] 实现 `OliveSequence*``OliveViewerOutput*` 的 C API。
- [ ] 这是渲染的入口:渲染子进程需要知道哪个 `ViewerOutput` 是输出目标。
---
## 5. 风险与回退
| 风险 | 对策 |
|---|---|
| Node.h 解耦工作量过大,影响面太广 | **分阶段**:第一阶段只做"编译期解耦"(移除 include),不改虚函数签名。若仍然困难,允许 `libolivenode.so``liboliverender.so` 暂时合并为 `libolive-engine.so`,后续再拆分。 |
| `QObject` 信号槽跨动态库 | Qt 信号槽跨动态库在正确链接 Qt 的情况下工作正常。确保所有含 `Q_OBJECT` 的类在动态库内被 `moc` 处理。 |
| `NodeValueTable` 等模板类难以导出 C 接口 | 不在 C API 中暴露模板类。`NodeTraverser` 的遍历结果(`NodeValueTable`)在 C++ 内部处理,C API 只提供高阶函数如 `olive_node_graph_evaluate_at_time`。 |
| 序列化 XML 格式变更 | C API 中的 `load_xml`/`save_xml` 直接使用现有的 `ProjectSerializer`,XML 格式完全不变,向下兼容。 |
| `Footage` 节点持有 `Decoder` | `Footage` 内部持有 `DecoderPtr`C API 不暴露 Decoder 细节,只暴露 `Footage` 的文件路径设置/获取。 |
@@ -1,406 +0,0 @@
# liboliverender.so — 渲染引擎抽象
> **依赖**`libolivecore.so`, `libolivecodec.so`, `libolivenode.so`
> **外部依赖**Qt::Core, Qt::OpenGL, OpenColorIO, OpenGL
> **包含源码**`app/render/`(不含 OpenGL 具体后端平台代码的抽象层)
> **当前状态**:单体 OBJECT 库的一部分,直接管理 `RenderThread`、`RenderProcessor`、`OpenGLRenderer`
> **改造难度**:⭐⭐⭐⭐(困难,与 Node 耦合深)
---
## 1. 当前状态分析
`app/render/` 是渲染系统的核心,负责将节点图转换为可显示的帧/音频。当前架构:
| 组件 | 说明 |
|---|---|
| `rendermanager.h/cpp` | 渲染管理单例,管理 `RenderThread` 和缓存 |
| `renderprocessor.h/cpp` | 节点图遍历 + 渲染作业生成(继承 `NodeTraverser` |
| `renderer.h/cpp` | 渲染器抽象基类(`Renderer` |
| `opengl/openglrenderer.h/cpp` | OpenGL 渲染后端 |
| `job/*.h` | 各种渲染作业类型(ShaderJob, FootageJob, GenerateJob 等) |
| `previewautocacher.h/cpp` | 预览自动缓存 |
| `rendercache.h/cpp` | 渲染缓存框架 |
**关键设计决策**
本方案中,**实际的 GPU 渲染发生在 `olive-renderer` 子进程中**,不在主进程的 `liboliverender.so` 中。因此 `liboliverender.so` 的角色需要重新定位:
- **在主进程中**`liboliverender.so` 提供轻量的 **渲染客户端** 功能:节点图序列化、渲染参数打包、共享内存创建、子进程启动协调。
- **在子进程中**`olive-renderer` 可执行文件链接 `liboliverender.so`(或静态链接其代码),执行实际的 `RenderProcessor` + `OpenGLRenderer`
也就是说,`liboliverender.so` 既服务于主进程(IPC 客户端),也服务于子进程(渲染服务端)。但通过编译选项或子目录拆分,可以在主进程中只包含轻量客户端代码。
**简化方案**`liboliverender.so` 包含完整的渲染逻辑(包括 `RenderProcessor``Renderer` 抽象),但主进程中的 `RenderManager` 不再直接调用它,而是通过 C API 启动 `olive-renderer` 子进程。子进程自身可以静态链接或动态链接 `liboliverender.so` 来执行渲染。
---
## 2. C API 设计
### 2.1 头文件:`c_api/include/olive/render_api.h`
```c
#ifndef OLIVE_RENDER_API_H
#define OLIVE_RENDER_API_H
#ifdef __cplusplus
extern "C" {
#endif
#include "core_api.h"
#include "node_api.h"
#define OLIVE_RENDER_API_VERSION 1
#ifdef OLIVE_BUILDING_RENDER
# define OLIVE_RENDER_API __attribute__((visibility("default")))
#else
# define OLIVE_RENDER_API
#endif
/* ========== 枚举 ========== */
typedef enum {
OLIVE_RENDER_MODE_OFFLINE = 0, // 最高质量(导出)
OLIVE_RENDER_MODE_ONLINE, // 实时预览(允许降低精度)
} OliveRenderMode;
typedef enum {
OLIVE_RENDER_BACKEND_OPENGL = 0,
OLIVE_RENDER_BACKEND_DUMMY,
} OliveRenderBackend;
/* ========== 不透明类型 ========== */
typedef struct OliveRenderContext OliveRenderContext;
typedef struct OliveRenderTicket OliveRenderTicket;
typedef struct OliveRenderParams OliveRenderParams;
/* ========== 渲染参数结构体 ========== */
typedef struct {
OliveNodeGraph* node_graph;
const char* output_node_id; // 通常为 ViewerOutput 的 ID
OliveRational time;
OliveVideoParams video_params;
OliveAudioParams audio_params;
OliveRenderMode mode;
OliveRenderBackend backend;
const char* color_reference_space; // 可为 nullptr
const char* color_display_space; // 可为 nullptr
OliveSize force_size; // {0,0} 表示不强制
OlivePixelFormat force_format; // INVALID 表示不强制
} OliveRenderFrameParams;
typedef struct {
OliveNodeGraph* node_graph;
const char* output_node_id;
OliveRational start;
OliveRational duration;
OliveAudioParams audio_params;
OliveRenderMode mode;
} OliveRenderAudioParams;
/* ========== API 版本 ========== */
OLIVE_RENDER_API int olive_render_api_version(void);
/* ========== 渲染上下文(用于本地/同进程渲染,或子进程内部) ========== */
OLIVE_RENDER_API OliveRenderContext* olive_render_context_create(OliveRenderBackend backend);
OLIVE_RENDER_API void olive_render_context_destroy(OliveRenderContext* ctx);
OLIVE_RENDER_API int olive_render_context_init(OliveRenderContext* ctx);
/* ========== 同步渲染(单帧) ========== */
// 渲染视频帧,结果写入 out_frameOliveFrame*,定义在 codec_api.h
OLIVE_RENDER_API int olive_render_frame_sync(OliveRenderContext* ctx,
const OliveRenderFrameParams* params,
void** out_frame_data, // 原始像素数据,需 olive_core_free
size_t* out_frame_size,
int* out_width,
int* out_height,
OlivePixelFormat* out_format);
// 渲染音频,结果写入 out_bufferOliveSampleBuffer*,定义在 core_api.h
OLIVE_RENDER_API int olive_render_audio_sync(OliveRenderContext* ctx,
const OliveRenderAudioParams* params,
OliveSampleBuffer** out_buffer);
/* ========== 异步渲染接口(用于子进程模型中的本地队列) ========== */
OLIVE_RENDER_API OliveRenderTicket* olive_render_frame_async(OliveRenderContext* ctx,
const OliveRenderFrameParams* params);
OLIVE_RENDER_API OliveRenderTicket* olive_render_audio_async(OliveRenderContext* ctx,
const OliveRenderAudioParams* params);
OLIVE_RENDER_API int olive_render_ticket_wait(OliveRenderTicket* ticket, int timeout_ms);
OLIVE_RENDER_API int olive_render_ticket_get_result_frame(OliveRenderTicket* ticket,
void** out_frame_data,
size_t* out_frame_size,
int* out_width,
int* out_height,
OlivePixelFormat* out_format);
OLIVE_RENDER_API void olive_render_ticket_destroy(OliveRenderTicket* ticket);
OLIVE_RENDER_API void olive_render_cancel_ticket(OliveRenderTicket* ticket);
/* ========== 节点图序列化辅助(供子进程使用) ========== */
// 将节点图序列化为适合渲染子进程消费的紧凑格式
OLIVE_RENDER_API char* olive_render_serialize_graph_for_render(OliveNodeGraph* graph,
const char* output_node_id,
size_t* out_len);
#ifdef __cplusplus
}
#endif
#endif // OLIVE_RENDER_API_H
```
### 2.2 实现要点
```cpp
// c_api/src/render_api.cpp
#include "olive/render_api.h"
#include "render/rendermanager.h"
#include "render/renderprocessor.h"
#include "render/renderer.h"
#include "render/opengl/openglrenderer.h"
#include "render/job/shaderjob.h"
#include "render/job/generatejob.h"
#include "render/job/footagejob.h"
#include "render/job/colortransformjob.h"
#include "render/job/samplejob.h"
#include "codec/frame.h"
#include "node/output/viewer/viewer.h"
#include "node/project.h"
struct OliveRenderContext {
olive::Renderer* renderer = nullptr;
olive::DecoderCache* decoder_cache = nullptr;
olive::ShaderCache* shader_cache = nullptr;
};
struct OliveRenderTicket {
olive::RenderTicketPtr impl;
};
extern "C" {
OliveRenderContext* olive_render_context_create(OliveRenderBackend backend) {
try {
auto* ctx = new OliveRenderContext();
if (backend == OLIVE_RENDER_BACKEND_OPENGL) {
ctx->renderer = new olive::OpenGLRenderer();
} else {
// ctx->renderer = new olive::DummyRenderer();
}
ctx->decoder_cache = new olive::DecoderCache();
ctx->shader_cache = new olive::ShaderCache();
return ctx;
} catch (...) {
return nullptr;
}
}
void olive_render_context_destroy(OliveRenderContext* ctx) {
if (!ctx) return;
delete ctx->shader_cache;
delete ctx->decoder_cache;
if (ctx->renderer) {
ctx->renderer->Destroy();
delete ctx->renderer;
}
delete ctx;
}
int olive_render_context_init(OliveRenderContext* ctx) {
if (!ctx || !ctx->renderer) return OLIVE_ERROR_INVALID;
try {
if (!ctx->renderer->Init()) return OLIVE_ERROR_GENERIC;
ctx->renderer->PostInit();
return OLIVE_OK;
} catch (...) {
return OLIVE_ERROR_GENERIC;
}
}
int olive_render_frame_sync(OliveRenderContext* ctx,
const OliveRenderFrameParams* params,
void** out_frame_data,
size_t* out_frame_size,
int* out_width,
int* out_height,
OlivePixelFormat* out_format) {
if (!ctx || !params) return OLIVE_ERROR_INVALID;
try {
// 1. 找到输出节点
olive::Node* output_node = nullptr;
{
auto* cpp_graph = static_cast<olive::NodeGraph*>(params->node_graph); // 需要内部转换
// ... 查找 output_node_id 对应的节点 ...
}
// 2. 构造 RenderVideoParams
olive::RenderManager::RenderVideoParams vparams(
output_node,
ConvertToCpp(params->video_params),
ConvertToCpp(params->audio_params),
olive::Rational(params->time.num, params->time.den),
nullptr, // ColorManager,需从 graph 获取或传入
params->mode == OLIVE_RENDER_MODE_OFFLINE ? olive::RenderMode::kOffline : olive::RenderMode::kOnline
);
// 3. 创建 ticket 并执行
auto ticket = std::make_shared<olive::RenderTicket>();
ticket->Start();
olive::RenderProcessor::Process(ticket, ctx->renderer, ctx->decoder_cache, ctx->shader_cache);
// 4. 等待结果
ticket->WaitForFinished();
if (!ticket->HasResult()) return OLIVE_ERROR_GENERIC;
// 5. 提取帧数据
olive::FramePtr frame = ticket->Get().value<olive::FramePtr>();
if (!frame) return OLIVE_ERROR_GENERIC;
*out_width = frame->width();
*out_height = frame->height();
*out_format = ConvertToC(frame->format());
size_t data_size = frame->allocated_size();
void* data = malloc(data_size);
memcpy(data, frame->data(), data_size);
*out_frame_data = data;
*out_frame_size = data_size;
return OLIVE_OK;
} catch (...) {
return OLIVE_ERROR_GENERIC;
}
}
// ... 其他函数类似封装 ...
} // extern "C"
```
---
## 3. CMake 改造
```cmake
# app/render/CMakeLists.txt
set(RENDER_INTERNAL_SOURCES
rendermanager.cpp rendermanager.h
renderprocessor.cpp renderprocessor.h
renderer.cpp renderer.h
renderticket.cpp renderticket.h
rendercache.cpp rendercache.h
previewautocacher.cpp previewautocacher.h
colorprocessor.cpp colorprocessor.h
colorprocessorcache.cpp colorprocessorcache.h
diskmanager.cpp diskmanager.h
# ... job/ 目录下的所有文件
job/shaderjob.cpp job/shaderjob.h
job/generatejob.cpp job/generatejob.h
job/footagejob.cpp job/footagejob.h
job/colortransformjob.cpp job/colortransformjob.h
job/samplejob.cpp job/samplejob.h
job/cachejob.cpp job/cachejob.h
job/pluginjob.cpp job/pluginjob.h
job/acceleratedjob.cpp job/acceleratedjob.h
# ... opengl/ 目录
opengl/openglrenderer.cpp opengl/openglrenderer.h
opengl/openglshader.cpp opengl/openglshader.h
opengl/opengltexture.cpp opengl/opengltexture.h
# ... plugin/ 目录(OFX 插件专用渲染器)
plugin/pluginrenderer.cpp plugin/pluginrenderer.h
)
set(RENDER_API_SOURCES
${CMAKE_SOURCE_DIR}/c_api/src/render_api.cpp
)
add_library(oliverender SHARED
${RENDER_INTERNAL_SOURCES}
${RENDER_API_SOURCES}
)
target_compile_definitions(oliverender PRIVATE OLIVE_BUILDING_RENDER)
target_include_directories(oliverender
PRIVATE
${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/third_party/openfx/include
${CMAKE_SOURCE_DIR}/third_party/openfx/HostSupport/include
${CMAKE_SOURCE_DIR}/c_api/include
PUBLIC
$<INSTALL_INTERFACE:include>
)
target_link_libraries(oliverender
PUBLIC
olivenode
olivecodec
olivecore
oliveplugin
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::OpenGL
${OCIO_LIBRARIES}
OpenGL::GL
)
set_target_properties(oliverender PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
install(TARGETS oliverender DESTINATION lib)
install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/render_api.h DESTINATION include/olive)
```
---
## 4. 小步快跑实施步骤
### Step 0: 分离渲染客户端与服务端代码(2 天)
- [ ] 分析 `render/` 中哪些代码主进程需要(RenderManager 的排队/协调逻辑),哪些仅子进程需要(OpenGLRenderer、RenderProcessor)。
- [ ] 创建 `app/render/client/` 子目录,放置主进程专用的轻量代码(如 `RenderProcessLauncher`)。
- [ ] 确保 `render/` 的现有代码仍然可以编译为完整的库(子进程使用)。
**验收标准**`liboliverender.so` 编译成功,包含完整的渲染逻辑。
### Step 1: 将 render/ 独立为动态库(1 天)
- [ ] 修改 `app/render/CMakeLists.txt`,将 `render/` 从主 OBJECT 库移出,构建为 `oliverender SHARED`
- [ ] 处理 `shaders/` 目录的资源文件路径问题(子进程需要知道着色器文件位置)。
**验收标准**`liboliverender.so` 编译成功。
### Step 2: 最小 C API2 天)
- [ ] 实现 `olive_render_context_create/destroy/init`
- [ ] 实现 `olive_render_frame_sync`(同步渲染单帧)。
- [ ] 此 C API 主要供 `olive-renderer` 子进程内部使用(子进程加载 `liboliverender.so` 后调用)。
**验收标准**:可以编写一个命令行测试程序,加载 `liboliverender.so`,初始化 OpenGL,渲染一帧纯色。
### Step 3: 节点图序列化辅助(1 天)
- [ ] 实现 `olive_render_serialize_graph_for_render`
- [ ] 此函数供主进程调用,将目标 `ViewerOutput` 及其上游节点序列化为紧凑 XML。
**验收标准**:给定一个包含 ViewerOutput 的图,序列化后的 XML 可以被 `ProjectSerializer` 重新加载。
### Step 4: 异步 Ticket 接口(2 天)
- [ ] 实现 `olive_render_frame_async`, `olive_render_ticket_wait`, `olive_render_ticket_get_result_frame`
- [ ] 此接口用于子进程内部的并发渲染(一个子进程内可同时渲染多帧)。
---
## 5. 风险与回退
| 风险 | 对策 |
|---|---|
| `RenderProcessor` 深度依赖 `NodeTraverser`C API 难以表达遍历逻辑 | `olive_render_frame_sync` 是高阶封装,内部直接使用原有的 C++ `RenderProcessor`,C API 调用者无需了解遍历细节。 |
| OpenGL 上下文初始化在不同平台差异大 | 在子进程中处理平台差异(子进程使用 `QOffscreenSurface` + `QOpenGLContext`)。C API 中 `backend` 参数暂时只支持 `"opengl"`。 |
| `PreviewAutoCacher` 的复杂缓存逻辑 | `PreviewAutoCacher` 保留在主进程中(或完全移除,因为"用完即弃"的渲染模型下,缓存策略由主进程重新设计)。 |
| 子进程需要访问 `app/shaders/` 下的 GLSL 文件 | 通过命令行参数 `--shader-path` 将资源路径传递给子进程。打包时确保着色器文件与可执行文件一同分发。 |
| `ColorManager` 和 OCIO 配置 | 通过 C API 参数 `color_reference_space` / `color_display_space` 传递,子进程内部重建 `ColorManager`。 |
-256
View File
@@ -1,256 +0,0 @@
# liboliveui.so — UI 层
> **依赖**`libolivecore.so`, `libolivecodec.so`, `libolivenode.so`, `liboliverender.so`, `liboliveaudio.so`
> **外部依赖**Qt::Widgets, Qt::OpenGL, Qt::OpenGLWidgets, KDDockWidgets
> **包含源码**`app/widget/`, `app/panel/`, `app/window/`, `app/dialog/`, `app/tool/`, `app/ui/`
> **当前状态**:单体 OBJECT 库的一部分
> **改造难度**:⭐⭐⭐(中等,但代码量大)
---
## 1. 当前状态分析
UI 层是代码量最大的模块,但**耦合方向是单向的**:UI 依赖下层(node/render/codec),下层不依赖 UI。这使得 UI 层的拆分相对直接。
| 组件 | 说明 |
|---|---|
| `widget/` | 40+ 自定义 Qt Widget(节点视图、时间线、播放控制、颜色轮等) |
| `panel/` | 基于 KDDockWidgets 的可停靠面板包装 |
| `window/mainwindow/` | 主窗口 |
| `dialog/` | 模态对话框(导出、首选项、项目属性等) |
| `tool/` | 工具枚举 |
| `ui/` | 图标、光标、样式表、翻译资源 |
**关键问题**
- `Core` 类(`core.h/cpp`)混合了业务逻辑和 UI 逻辑(`StartGUI()`, `main_window_` 等)。
- `RenderManager``RenderTicketWatcher` 使用 Qt 信号通知 UI。
- 大量 UI 类直接包含 `node/``render/` 的 C++ 头文件。
**策略**
- `liboliveui.so` 的 C API 不需要非常完善,因为 UI 层**大概率仍然与主进程一同编译**(UI 是主进程的核心)。
- 但为了保持架构一致性,仍定义 C API 用于:
1. 第三方脚本/插件通过 C API 操作 UI(未来扩展)。
2. 单元测试通过 C API 驱动 UI(自动化测试)。
- **主进程中的 UI 代码可以继续使用 C++ 直接包含下层头文件**,不必全部改为 C API 调用。这是因为 UI 层在最顶层,不需要被其他模块依赖。
**修正策略**`liboliveui.so` 的拆分重点在于:
1. 将 UI 代码从单体 OBJECT 库移出,编译为独立的 `liboliveui.so`
2. 主进程显式加载 `liboliveui.so`
3. UI 层内部继续使用 C++ 直接调用下层(node/render 等),只在跨库边界处遵循 ABI 规则。
---
## 2. C API 设计(精简版)
UI 层的 C API 不需要覆盖所有 Widget,只需提供应用级入口和关键面板操作:
### 2.1 头文件:`c_api/include/olive/ui_api.h`
```c
#ifndef OLIVE_UI_API_H
#define OLIVE_UI_API_H
#ifdef __cplusplus
extern "C" {
#endif
#include "core_api.h"
#include "node_api.h"
#define OLIVE_UI_API_VERSION 1
#ifdef OLIVE_BUILDING_UI
# define OLIVE_UI_API __attribute__((visibility("default")))
#else
# define OLIVE_UI_API
#endif
/* ========== 不透明类型 ========== */
typedef struct OliveApplication OliveApplication;
typedef struct OliveMainWindow OliveMainWindow;
typedef struct OliveViewerPanel OliveViewerPanel;
typedef struct OliveTimelinePanel OliveTimelinePanel;
typedef struct OliveNodeEditorPanel OliveNodeEditorPanel;
/* ========== API 版本 ========== */
OLIVE_UI_API int olive_ui_api_version(void);
/* ========== 应用生命周期 ========== */
OLIVE_UI_API OliveApplication* olive_ui_app_create(int argc, char** argv);
OLIVE_UI_API int olive_ui_app_exec(OliveApplication* app);
OLIVE_UI_API void olive_ui_app_quit(OliveApplication* app);
OLIVE_UI_API void olive_ui_app_destroy(OliveApplication* app);
/* ========== 主窗口 ========== */
OLIVE_UI_API OliveMainWindow* olive_ui_main_window_create(OliveApplication* app);
OLIVE_UI_API void olive_ui_main_window_destroy(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_main_window_show(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_main_window_set_fullscreen(OliveMainWindow* win, int fullscreen);
/* ========== 项目操作 ========== */
OLIVE_UI_API int olive_ui_open_project(OliveMainWindow* win, const char* filename);
OLIVE_UI_API int olive_ui_save_project(OliveMainWindow* win, const char* filename);
OLIVE_UI_API int olive_ui_import_footage(OliveMainWindow* win, const char** filenames, int count);
/* ========== 查看器(Viewer ========== */
OLIVE_UI_API OliveViewerPanel* olive_ui_get_active_viewer(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_viewer_set_time(OliveViewerPanel* viewer, OliveRational time);
OLIVE_UI_API void olive_ui_viewer_play(OliveViewerPanel* viewer);
OLIVE_UI_API void olive_ui_viewer_pause(OliveViewerPanel* viewer);
OLIVE_UI_API void olive_ui_viewer_stop(OliveViewerPanel* viewer);
/* ========== 时间线 ========== */
OLIVE_UI_API OliveTimelinePanel* olive_ui_get_active_timeline(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_timeline_set_time(OliveTimelinePanel* timeline, OliveRational time);
OLIVE_UI_API void olive_ui_timeline_set_work_area(OliveTimelinePanel* timeline,
OliveRational in,
OliveRational out);
/* ========== 节点编辑器 ========== */
OLIVE_UI_API OliveNodeEditorPanel* olive_ui_get_node_editor(OliveMainWindow* win);
OLIVE_UI_API void olive_ui_node_editor_set_graph(OliveNodeEditorPanel* editor,
OliveNodeGraph* graph);
/* ========== 导出对话框 ========== */
OLIVE_UI_API int olive_ui_show_export_dialog(OliveMainWindow* win,
OliveViewerOutput* viewer_output,
const char* default_filename);
/* ========== 状态栏消息 ========== */
OLIVE_UI_API void olive_ui_show_status_message(OliveMainWindow* win,
const char* message,
int timeout_ms);
#ifdef __cplusplus
}
#endif
#endif // OLIVE_UI_API_H
```
---
## 3. CMake 改造
```cmake
# 由于 UI 层代码分散在 widget/, panel/, window/, dialog/, tool/, ui/ 多个目录,
# 需要在 app/CMakeLists.txt 中统一聚合。
set(UI_INTERNAL_SOURCES
# widget/
widget/viewer/viewerwidget.cpp widget/viewer/viewerwidget.h
widget/timelinewidget/timelinewidget.cpp widget/timelinewidget/timelinewidget.h
widget/nodeview/nodeview.cpp widget/nodeview/nodeview.h
# ... 所有 widget 源文件
# panel/
panel/viewer/viewerpanel.cpp panel/viewer/viewerpanel.h
panel/timeline/timelinepanel.cpp panel/timeline/timelinepanel.h
panel/node/nodepanel.cpp panel/node/nodepanel.h
# ... 所有 panel 源文件
# window/
window/mainwindow/mainwindow.cpp window/mainwindow/mainwindow.h
# dialog/
dialog/export/exportdialog.cpp dialog/export/exportdialog.h
dialog/preferences/preferencesdialog.cpp dialog/preferences/preferencesdialog.h
# ... 所有 dialog 源文件
# tool/
tool/tool.cpp tool/tool.h
# ui/ 资源(.qrc 等)
# ...
)
set(UI_API_SOURCES
${CMAKE_SOURCE_DIR}/c_api/src/ui_api.cpp
)
add_library(oliveui SHARED
${UI_INTERNAL_SOURCES}
${UI_API_SOURCES}
)
target_compile_definitions(oliveui PRIVATE OLIVE_BUILDING_UI)
target_include_directories(oliveui
PRIVATE
${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/c_api/include
${CMAKE_SOURCE_DIR}/ext/KDDockWidgets/src
PUBLIC
$<INSTALL_INTERFACE:include>
)
target_link_libraries(oliveui
PUBLIC
olivenode
oliverender
olivecodec
oliveaudio
olivecore
oliveplugin
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::OpenGL
Qt${QT_VERSION_MAJOR}::OpenGLWidgets
KDAB::kddockwidgets
)
set_target_properties(oliveui PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
install(TARGETS oliveui DESTINATION lib)
install(FILES ${CMAKE_SOURCE_DIR}/c_api/include/olive/ui_api.h DESTINATION include/olive)
```
---
## 4. 小步快跑实施步骤
### Step 0: 解耦 Core 类(2 天)
- [ ] 分析 `core.h/cpp` 中哪些属于 UI 逻辑(`StartGUI()`, `main_window_`, `ImportFiles()` 等),哪些属于业务逻辑。
- [ ] 将 UI 相关逻辑迁移到 `liboliveui.so` 内部的一个 `UiCore` 类中。
- [ ] 保留 `Core` 类中纯业务逻辑(如 `FootageFileDialogFilter`, `CreateNewSequenceForProject`)。
**验收标准**`core.h` 不再包含 `mainwindow.h``projectexplorer.h` 等 UI 头文件。
### Step 1: 聚合 UI 源码(1 天)
- [ ]`app/CMakeLists.txt` 中聚合所有 UI 相关源文件(widget/, panel/, window/, dialog/, tool/, ui/)。
- [ ] 确保 `liboliveui.so` 可以编译。
**验收标准**`liboliveui.so` 编译成功。
### Step 2: 主进程加载 UI 库(1 天)
- [ ] 修改 `main.cpp`:先通过 `ModuleLoader` 加载 `liboliveui.so`,然后调用 `olive_ui_app_create``olive_ui_main_window_create`
- [ ] 若动态加载失败,回退到静态链接模式。
**验收标准**:主程序启动时日志显示成功加载 `ui` 模块,并正常显示主窗口。
### Step 3: C API 实现(按需,23 天)
- [ ] 实现 `olive_ui_app_create/exec/quit/destroy`
- [ ] 实现 `olive_ui_main_window_create/show`
- [ ] 实现项目操作:`open_project`, `save_project`, `import_footage`
- [ ] 其他 UI C API 根据测试/脚本需求逐步实现。
**验收标准**:可以通过一个外部测试程序加载 `liboliveui.so` 并打开主窗口。
---
## 5. 风险与回退
| 风险 | 对策 |
|---|---|
| UI 代码量巨大,聚合时容易遗漏源文件 | 编写脚本自动收集 `widget/`, `panel/`, `window/`, `dialog/` 下的所有 `.cpp`/`.h` 文件,或在 CMake 中保持原有的 `add_subdirectory` 结构,只是最终输出为 SHARED 而非 OBJECT。 |
| KDDockWidgets 的符号跨动态库 | KDDockWidgets 以静态库形式链接进 `liboliveui.so`,其符号不外泄。确保 `liboliveui.so``CXX_VISIBILITY_PRESET hidden`。 |
| Qt 资源文件(.qrc)在动态库中的加载 | 将 `.qrc` 编译进 `liboliveui.so`,Qt 的资源系统在动态库中工作正常。确保 `Q_INIT_RESOURCE()` 在库加载时被调用。 |
| `Core` 类的信号槽跨模块 | `Core` 保留在主进程,`UiCore``liboliveui.so` 中。两者通过 C API 或 Qt 的跨进程信号(如果未来需要)通信。初期保持简单:主进程直接调用 UI 的 C API。 |
@@ -1,692 +0,0 @@
# olive-renderer — 多进程渲染可执行文件
> **类型**:独立可执行文件(非动态库)
> **依赖**`libolivecore.so`, `libolivecodec.so`, `libolivenode.so`, `liboliverender.so`(可静态或动态链接)
> **外部依赖**Qt::Core, OpenGL, FFmpeg, OpenColorIO
> **核心特征****"用完即弃"**——每帧(或每几帧)启动一个新进程,渲染完成后立即退出
> **改造难度**:⭐⭐⭐(中等,逻辑复杂但隔离清晰)
---
## 1. 设计哲学:用完即弃(Fire-and-Forget
传统多进程渲染架构通常维护一个**常驻子进程**,通过复杂的 IPC 协议进行状态同步、心跳检测、崩溃恢复。这种架构虽然成熟,但引入了以下复杂性:
- **状态同步**:主进程和子进程的节点图、缓存、参数必须保持一致。
- **锁与并发**:共享内存的读写需要锁或原子操作。
- **崩溃恢复**:子进程崩溃后需要重建状态、重同步节点图。
- **生命周期管理**:启动、握手、心跳、优雅退出、强制杀死的完整状态机。
本方案采用**激进的简化策略**
> **每一帧渲染任务 = 一个全新的操作系统进程**。进程接收完整的渲染输入,执行渲染,输出结果,然后 `exit(0)`。
### 1.1 优势
| 优势 | 说明 |
|---|---|
| **零锁** | 进程之间不共享任何可变状态(除了只读的共享内存输出区),无需任何互斥锁、信号量、条件变量。 |
| **零状态同步** | 每帧的输入都是自包含的(节点图 XML + 渲染参数),子进程无需维护任何跨帧状态。 |
| **自动崩溃隔离** | 某一帧的渲染崩溃(OFX 插件 segfault、GPU 驱动错误)只会影响该进程,主进程和其他帧完全不受影响。 |
| **资源自动回收** | 进程退出后,操作系统自动回收其所有资源(内存、GPU 上下文、文件句柄、解码器实例),无需显式清理。 |
| **可预测性** | 没有状态泄漏、没有内存碎片累积、没有僵尸缓存,每一帧都在干净的环境中渲染。 |
| **易于调试** | 可以单独运行 `olive-renderer` 命令行重放某一帧的渲染,无需启动完整 GUI。 |
### 1.2 挑战与回退
| 挑战 | 分析 | 回退策略 |
|---|---|---|
| **进程启动开销** | `QProcess::start()` + OpenGL 上下文初始化可能需要 50–200ms | 若实测开销过高,采用 **"批处理模式"**:一个进程渲染 N 帧(如 5 帧),然后退出。或预启动一个进程池,但每个进程仍只服务一个批次后自杀。 |
| **节点图序列化开销** | 每帧都序列化完整节点图可能耗时 | 节点图 XML 在主进程缓存,相同图只序列化一次;仅参数变化时发送增量更新(即使进程用完即弃,输入数据仍可复用)。 |
| **GPU 上下文反复创建** | OpenGL 上下文创建/销毁开销较大 | 采用 **批处理模式** 摊销开销;或在支持的平台使用 EGL/GLES 的轻量上下文。 |
| **共享内存创建开销** | 每帧创建新的 shm 对象 | 使用 **内存映射临时文件** 替代 POSIX shm,创建开销更低;或主进程预分配一组循环缓冲区。 |
| **音频连续性** | 音频需要连续播放,逐帧进程可能导致间隙 | 音频采用 **批处理模式**:一个进程渲染 0.5–1 秒的音频块,而非每帧一个进程。 |
---
## 2. 进程模型详解
### 2.1 单帧模式(默认)
```
主进程(UI 线程或工作线程)
│ 1. 序列化节点图(若未缓存则生成 XML)
│ 2. 创建共享内存 / 临时文件
│ 3. 组装渲染参数
┌─────────────────────────────────────┐
│ QProcess::start("olive-renderer", │
│ ["--mode=frame", │
│ "--node-graph=/tmp/g_123.xml", │
│ "--time=1001/30000", │
│ "--output-shm=/olive_r_123"]) │
└─────────────────────────────────────┘
│ 4. 等待子进程结束(阻塞或异步)
│ QProcess::waitForFinished(timeout_ms)
子进程启动 ──────────────────────────────► 子进程退出
│ │
│ a. 解析命令行参数 │
│ b. 加载节点图 XML │
│ c. 初始化 Qt Core + OpenGL │
│ d. 执行 RenderProcessor │
│ e. 将帧写入共享内存 │
│ f. 输出结果 JSON 到 stdout │
│ g. exit(0) │
▼ ▼
主进程读取共享内存 ──────────────────────► 主进程释放共享内存
│ 5. 将帧数据上传到 GPU Texture 或显示
ViewerWidget 更新
```
### 2.2 批处理模式(性能回退)
当实测单帧模式开销过高时,启用批处理:
```
主进程
│ 渲染帧 #1, #2, #3, #4, #5
启动 olive-renderer
--mode=batch
--frames=5
--times=0/30,1/30,2/30,3/30,4/30
--output-shm=/olive_r_batch_1
--output-shm-size=165888000 (5 * 33MB)
子进程依次渲染 5 帧,全部写入同一块共享内存的不同偏移
输出 JSON 数组包含 5 个结果
exit(0)
```
---
## 3. 命令行接口
### 3.1 参数定义
```
olive-renderer [选项]
全局选项:
--backend=<backend> 渲染后端:opengl(默认), dummy
--shader-path=<path> 着色器资源目录(默认:可执行文件同级目录下的 shaders/)
--ocio-config=<path> OpenColorIO 配置文件路径
--verbose 输出详细日志到 stderr
单帧模式(--mode=frame):
--mode=frame
--node-graph=<path> 节点图 XML 文件路径
--output-node=<id> 输出节点 ID(默认:图中的第一个 ViewerOutput
--time=<rational> 渲染时间点,如 "1001/30000"
--video-params=<json> 视频参数 JSON,如 '{"width":1920,"height":1080,"format":"rgba32f"}'
--audio-params=<json> 音频参数 JSON
--color-ref=<space> 参考色彩空间
--color-display=<space> 显示色彩空间
--output-shm=<name> POSIX 共享内存名称(如 "/olive_r_123")或临时文件路径
--output-shm-size=<bytes> 共享内存大小
--output-stdout 将帧数据 base64 编码输出到 stdout(仅小帧/测试用)
批处理模式(--mode=batch):
--mode=batch
--node-graph=<path>
--output-node=<id>
--times=<csv> 逗号分隔的时间点列表,如 "0/30,1/30,2/30"
--video-params=<json>
--audio-params=<json>
--output-shm=<name>
--output-shm-size=<bytes>
音频模式(--mode=audio):
--mode=audio
--node-graph=<path>
--output-node=<id>
--start=<rational> 起始时间
--duration=<rational> 持续时间
--audio-params=<json>
--output-shm=<name>
--output-shm-size=<bytes>
```
### 3.2 使用示例
```bash
# 单帧渲染
olive-renderer \
--mode=frame \
--node-graph=/tmp/project_graph.xml \
--output-node=ViewerOutput1 \
--time=1001/30000 \
--video-params='{"width":1920,"height":1080,"format":"rgba32f","channel_count":4}' \
--output-shm=/olive_frame_12345 \
--output-shm-size=33177600
# 批处理渲染 5 帧
olive-renderer \
--mode=batch \
--node-graph=/tmp/project_graph.xml \
--times="0/24,1/24,2/24,3/24,4/24" \
--output-shm=/olive_batch_67890 \
--output-shm-size=165888000
```
---
## 4. 输出格式
### 4.1 标准输出(stdout
子进程将渲染结果以 **单行 JSON** 输出到 stdout,然后退出。
**单帧成功**
```json
{"status":"ok","mode":"frame","time":"1001/30000","width":1920,"height":1080,"format":"rgba32f","pixel_format_id":28,"shm_name":"/olive_frame_12345","data_offset":256,"data_size":33177600,"linesize":7680,"render_time_ms":42}
```
**批处理成功**
```json
{"status":"ok","mode":"batch","frame_count":5,"frames":[{"time":"0/24","data_offset":256,"data_size":33177600},{"time":"1/24","data_offset":33178056,"data_size":33177600},...],"render_time_ms":180}
```
**错误**
```json
{"status":"error","error_code":"decoder_failure","message":"Failed to open decoder for footage 'clip001.mp4': codec not found","time":"1001/30000"}
```
**被取消**(主进程 kill 时不会收到,因为进程已死;但对于批处理中的内部取消):
```json
{"status":"cancelled","frames_completed":3,"frames_total":5}
```
### 4.2 标准错误(stderr
- `--verbose` 模式下,详细的调试日志输出到 stderr。
- 错误信息同时出现在 stderr(人类可读)和 stdout JSON(机器可读)中。
### 4.3 退出码
| 退出码 | 含义 |
|---|---|
| 0 | 渲染成功 |
| 1 | 通用错误 |
| 2 | 无效参数 |
| 3 | 初始化失败(OpenGL/OCIO 等) |
| 4 | 节点图加载失败 |
| 5 | 渲染过程中出错(解码失败、着色器编译失败等) |
| 6 | 输出写入失败(共享内存不足等) |
| 130 | 被信号中断(SIGINT,即主进程 kill |
| 137 | 被 SIGKILL 终止 |
---
## 5. 内部架构
```
olive-renderer (main.cpp)
┌─────────────────────────────┐
│ 1. 解析命令行参数 │
│ (QCommandLineParser) │
└─────────────┬───────────────┘
┌─────────────▼───────────────┐
│ 2. 初始化 QCoreApplication │
│ (无 GUI,无事件循环) │
└─────────────┬───────────────┘
┌─────────────▼───────────────┐
│ 3. 加载节点图 XML │
│ ProjectSerializer::Load() │
│ 找到目标 ViewerOutput │
└─────────────┬───────────────┘
┌─────────────▼───────────────┐
│ 4. 初始化渲染后端 │
│ OpenGL: QOffscreenSurface │
│ + QOpenGLContext │
│ Dummy: 空实现 │
└─────────────┬───────────────┘
┌─────────────▼───────────────┐
│ 5. 执行渲染 │
│ RenderProcessor::Process()│
│ 遍历节点图 → 生成帧 │
└─────────────┬───────────────┘
┌─────────────▼───────────────┐
│ 6. 写入共享内存 │
│ mmap/shm_open/MapViewOfFile│
│ 写入 ShmHeader + 像素数据 │
└─────────────┬───────────────┘
┌─────────────▼───────────────┐
│ 7. 输出 JSON 结果到 stdout │
│ 8. 清理(可选,因即将 exit) │
│ 9. return status; │
└─────────────────────────────┘
```
### 5.1 主函数伪代码
```cpp
// app/render/renderer_main.cpp
#include <QCoreApplication>
#include <QCommandLineParser>
#include <QFile>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QElapsedTimer>
#include "olive/render_api.h"
#include "olive/node_api.h"
int main(int argc, char** argv) {
QCoreApplication app(argc, argv);
QCommandLineParser parser;
// 定义命令行选项
parser.addOption({"mode", "Render mode: frame, batch, audio", "mode", "frame"});
parser.addOption({"node-graph", "Node graph XML file", "path"});
parser.addOption({"output-node", "Output node ID", "id"});
parser.addOption({"time", "Frame time (rational)", "time"});
parser.addOption({"times", "Batch frame times (csv)", "csv"});
parser.addOption({"video-params", "Video params JSON", "json"});
parser.addOption({"audio-params", "Audio params JSON", "json"});
parser.addOption({"output-shm", "Output shared memory name", "name"});
parser.addOption({"output-shm-size", "Output shared memory size", "bytes"});
parser.addOption({"backend", "Render backend", "backend", "opengl"});
parser.addOption({"shader-path", "Shader directory path", "path"});
parser.addOption({"verbose", "Verbose logging"});
parser.process(app);
// 验证必需参数
if (!parser.isSet("node-graph") || !parser.isSet("output-shm")) {
OutputError("Missing required arguments: --node-graph and --output-shm");
return 2;
}
// 1. 加载节点图
OliveNodeGraph* graph = olive_node_graph_create();
QFile xml_file(parser.value("node-graph"));
if (!xml_file.open(QIODevice::ReadOnly)) {
OutputError("Failed to open node graph file");
return 4;
}
QByteArray xml_data = xml_file.readAll();
if (olive_node_graph_load_xml(graph, xml_data.constData(), xml_data.size()) != OLIVE_OK) {
OutputError("Failed to parse node graph XML");
return 4;
}
// 2. 找到输出节点
const char* output_node_id = parser.value("output-node").toUtf8().constData();
OliveNode* output_node = olive_node_graph_find_node(graph, output_node_id);
if (!output_node) {
// 如果没指定,找第一个 ViewerOutput
// ...
}
// 3. 初始化渲染上下文
OliveRenderBackend backend = parser.value("backend") == "dummy"
? OLIVE_RENDER_BACKEND_DUMMY
: OLIVE_RENDER_BACKEND_OPENGL;
OliveRenderContext* ctx = olive_render_context_create(backend);
if (!ctx) {
OutputError("Failed to create render context");
return 3;
}
if (olive_render_context_init(ctx) != OLIVE_OK) {
OutputError("Failed to initialize render backend");
return 3;
}
// 4. 准备共享内存
QString shm_name = parser.value("output-shm");
size_t shm_size = parser.value("output-shm-size").toULongLong();
void* shm_ptr = MapSharedMemory(shm_name, shm_size);
if (!shm_ptr) {
OutputError("Failed to map shared memory");
return 6;
}
// 5. 执行渲染
QElapsedTimer timer;
timer.start();
QString mode = parser.value("mode");
QJsonObject result;
if (mode == "frame") {
// 单帧渲染
OliveRenderFrameParams params = ParseFrameParams(parser, graph, output_node);
void* frame_data = nullptr;
size_t frame_size = 0;
int w, h;
OlivePixelFormat fmt;
int err = olive_render_frame_sync(ctx, &params, &frame_data, &frame_size, &w, &h, &fmt);
if (err == OLIVE_OK) {
// 写入共享内存
WriteFrameToShm(shm_ptr, frame_data, frame_size, w, h, fmt);
olive_core_free(frame_data);
result["status"] = "ok";
result["mode"] = "frame";
result["width"] = w;
result["height"] = h;
result["shm_name"] = shm_name;
result["data_offset"] = 256;
result["data_size"] = static_cast<qint64>(frame_size);
} else {
result["status"] = "error";
result["message"] = olive_core_last_error_string();
}
} else if (mode == "batch") {
// 批处理渲染...
}
result["render_time_ms"] = timer.elapsed();
// 6. 输出 JSON
std::cout << QJsonDocument(result).toJson(QJsonDocument::Compact).toStdString() << std::endl;
// 7. 清理
UnmapSharedMemory(shm_ptr, shm_size);
olive_render_context_destroy(ctx);
olive_node_graph_destroy(graph);
return result["status"].toString() == "ok" ? 0 : 5;
}
```
---
## 6. 共享内存实现
### 6.1 跨平台封装
```cpp
// app/render/shared_memory.h
#pragma once
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
void* olive_shm_create(const char* name, size_t size);
void* olive_shm_open(const char* name, size_t size);
void olive_shm_close(void* ptr, size_t size);
void olive_shm_unlink(const char* name);
#ifdef __cplusplus
}
#endif
```
### 6.2 POSIX 实现
```cpp
#include "shared_memory.h"
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
void* olive_shm_create(const char* name, size_t size) {
int fd = shm_open(name, O_RDWR | O_CREAT, 0666);
if (fd < 0) return nullptr;
if (ftruncate(fd, size) < 0) {
close(fd);
return nullptr;
}
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
return ptr;
}
void* olive_shm_open(const char* name, size_t size) {
int fd = shm_open(name, O_RDWR, 0666);
if (fd < 0) return nullptr;
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
close(fd);
return ptr;
}
void olive_shm_close(void* ptr, size_t size) {
if (ptr) munmap(ptr, size);
}
void olive_shm_unlink(const char* name) {
shm_unlink(name);
}
```
### 6.3 Windows 实现
```cpp
#include <windows.h>
void* olive_shm_create(const char* name, size_t size) {
HANDLE hMap = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr,
PAGE_READWRITE, 0, static_cast<DWORD>(size), name);
if (!hMap) return nullptr;
return MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, size);
// 注意:句柄需要保存以便后续关闭,此处简化
}
void olive_shm_close(void* ptr, size_t size) {
(void)size;
if (ptr) UnmapViewOfFile(ptr);
}
```
---
## 7. 主进程中的集成
### 7.1 渲染一帧的封装
```cpp
// app/render/render_process_launcher.h
#pragma once
#include <QString>
#include <QProcess>
#include <QJsonObject>
#include "olive/core_api.h"
#include "olive/node_api.h"
namespace olive {
class RenderProcessLauncher {
public:
struct FrameResult {
bool success = false;
QString error_message;
int width = 0;
int height = 0;
OlivePixelFormat format = OLIVE_PIXEL_FMT_INVALID;
QString shm_name;
size_t data_offset = 0;
size_t data_size = 0;
int render_time_ms = 0;
};
// 渲染单帧,阻塞直到子进程结束
static FrameResult RenderFrameSync(const QString& nodeGraphXmlPath,
const QString& outputNodeId,
OliveRational time,
const OliveVideoParams& videoParams,
const OliveAudioParams& audioParams,
OliveRenderMode mode,
int timeoutMs = 30000);
// 渲染单帧,异步(返回 QProcess*,调用方连接 finished 信号)
static QProcess* RenderFrameAsync(const QString& nodeGraphXmlPath,
const QString& outputNodeId,
OliveRational time,
const OliveVideoParams& videoParams,
const OliveAudioParams& audioParams,
OliveRenderMode mode);
private:
static QString BuildShmName();
static bool CreateShm(const QString& name, size_t size, void** outPtr);
static void DestroyShm(const QString& name, void* ptr, size_t size);
};
} // namespace olive
```
### 7.2 使用示例
```cpp
// ViewerWidget 中请求渲染当前帧
void ViewerWidget::RequestFrameAtTime(OliveRational time) {
// 1. 确保节点图 XML 已缓存
if (cached_graph_xml_path_.isEmpty()) {
cached_graph_xml_path_ = SerializeNodeGraphToTempFile(viewer_output_);
}
// 2. 创建共享内存
QString shm_name = RenderProcessLauncher::BuildShmName();
size_t shm_size = CalculateFrameSize(video_params_);
// 3. 启动子进程(异步)
QProcess* proc = RenderProcessLauncher::RenderFrameAsync(
cached_graph_xml_path_,
olive_node_get_id(reinterpret_cast<OliveNode*>(viewer_output_)),
time,
video_params_,
audio_params_,
OLIVE_RENDER_MODE_ONLINE
);
// 4. 连接完成信号
connect(proc, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
this, [this, proc, shm_name, shm_size](int exitCode, QProcess::ExitStatus status) {
OnRenderProcessFinished(proc, shm_name, shm_size, exitCode, status);
});
}
void ViewerWidget::OnRenderProcessFinished(QProcess* proc,
const QString& shm_name,
size_t shm_size,
int exitCode,
QProcess::ExitStatus status) {
if (status == QProcess::CrashExit) {
qWarning() << "Renderer process crashed for frame";
// 不需要恢复状态,直接丢弃这一帧,UI 保持上一帧
proc->deleteLater();
return;
}
// 解析 stdout JSON
QByteArray stdout_data = proc->readAllStandardOutput();
QJsonObject result = QJsonDocument::fromJson(stdout_data).object();
if (result["status"].toString() == "ok") {
// 从共享内存读取帧
void* shm_ptr = olive_shm_open(shm_name.toUtf8().constData(), shm_size);
if (shm_ptr) {
DisplayFrameFromShm(shm_ptr, result);
olive_shm_close(shm_ptr, shm_size);
olive_shm_unlink(shm_name.toUtf8().constData());
}
} else {
qWarning() << "Render error:" << result["message"].toString();
}
proc->deleteLater();
}
```
---
## 8. 小步快跑实施步骤
### Step 0: 验证进程启动开销(1 天)
- [ ] 编写一个最小测试程序 `test_process_spawn.cpp`,只测量 `QProcess::start()` + `waitForStarted()` 的耗时。
- [ ] 加上 OpenGL 上下文初始化(`QOffscreenSurface` + `QOpenGLContext`)测量总耗时。
- [ ] 在目标平台(开发机)上测试,记录数据。
**验收标准**:得到精确的单进程启动耗时数据,作为是否采用"用完即弃"或"批处理模式"的依据。
### Step 1: 创建 olive-renderer 可执行文件目标(1 天)
- [ ] 新增 `app/render/renderer_main.cpp`
- [ ]`app/CMakeLists.txt` 中新增 `add_executable(olive-renderer ...)`
- [ ] 链接 `oliverender`, `olivenode`, `olivecodec`, `olivecore`, `Qt::Core`
**验收标准**`olive-renderer` 编译成功,运行 `olive-renderer --help` 输出用法信息。
### Step 2: 实现命令行解析与节点图加载(1 天)
- [ ] 使用 `QCommandLineParser` 解析所有参数。
- [ ] 实现节点图 XML 加载(调用 `olive_node_graph_load_xml`)。
**验收标准**`olive-renderer --node-graph=test.xml --output-shm=/test` 能成功加载节点图并找到 ViewerOutput。
### Step 3: 实现共享内存读写(1 天)
- [ ] 实现 `olive_shm_create/open/close/unlink`POSIX + Windows)。
- [ ] 定义 `ShmHeader` 二进制布局。
**验收标准**:主进程创建 SHM,子进程写入数据,主进程读取并校验 CRC。
### Step 4: 单帧端到端渲染(2 天)
- [ ]`olive-renderer` 中初始化 OpenGL 上下文。
- [ ] 调用 `olive_render_frame_sync` 渲染一帧。
- [ ] 将结果写入共享内存。
- [ ] 输出 JSON 到 stdout。
- [ ] 在主进程中编写 `RenderProcessLauncher::RenderFrameSync` 测试。
**验收标准**:主进程可以成功渲染一帧纯色/测试图,并在 Viewer 中显示。
### Step 5: 集成到 ViewerWidget2 天)
- [ ] 修改 `ViewerWidget` 的帧请求逻辑,从直接调用 `RenderManager` 改为启动 `olive-renderer`
- [ ] 处理异步完成信号,从共享内存读取帧并显示。
- [ ] 处理子进程崩溃(忽略该帧,保持上一帧显示)。
**验收标准**:拖动时间线时,Viewer 能实时显示渲染结果(可能有延迟,但功能正确)。
### Step 6: 批处理模式(2 天,按需)
- [ ] 若 Step 0 的测试显示单帧开销过高,实现 `--mode=batch`
- [ ] 修改 `RenderProcessLauncher` 支持批处理。
### Step 7: 导出集成(2 天)
- [ ] 修改导出任务(`task/export/`),使用 `olive-renderer` 子进程逐帧渲染,然后编码。
- [ ] 导出天然适合批处理模式(可以一次性渲染 10–50 帧)。
---
## 9. 风险与回退
| 风险 | 对策 |
|---|---|
| 进程启动开销导致实时预览 < 10fps | 启用批处理模式,每 3–5 帧一个进程;或预启动一个进程池(但每个进程仍只服务一个批次后自杀)。 |
| OpenGL 驱动不支持离屏渲染 | 在 Linux 上使用 `EGL` 替代 `QOffscreenSurface`;在 Windows 上使用 `WGL` pbuffer;在 macOS 上使用 `CGL` pixel buffer。 |
| 共享内存名称冲突 | 使用 `QUuid::createUuid()` 生成唯一名称,格式为 `/olive_<pid>_<uuid>`。 |
| 共享内存泄漏(子进程崩溃后未 unlink) | 主进程在启动子进程前注册一个定时器,若子进程异常退出,5 秒后自动 `shm_unlink`。 |
| 磁盘空间不足(临时文件方案) | 渲染前检查磁盘空间,不足时返回错误码 6。 |
| 节点图 XML 过大导致解析慢 | 启用增量序列化:只序列化自上次以来变更的节点和参数。 |
| OFX 插件需要持久化状态 | OFX 插件实例不跨帧持久化,每帧重新创建。若某些插件初始化极慢,在 C API 中提供 `olive_plugin_instance_serialize_state` 接口,将状态快照传给下一帧的进程。 |
@@ -1,677 +0,0 @@
# C API 设计规范总纲
> **必读**:本文件定义了所有 Olive/Oak 动态库的 C API 设计约定。`01-` 到 `07-` 各模块的 API 均遵循此规范。
---
## 1. 设计原则
### 1.1 不透明指针(Opaque Pointer
所有 C++ 对象在 C 接口中均隐藏实现,仅暴露为 `struct` 的前向声明:
```c
// 公共头文件(.h)中
typedef struct OliveNodeGraph OliveNodeGraph; // 只有声明,无定义
// 实现文件(.cpp)中
struct OliveNodeGraph {
olive::NodeGraph* impl; // 实际的 C++ 对象
};
```
外部代码只能操作指针,无法解引用或 sizeof。
### 1.2 纯 C 接口
- 函数名使用 `snake_case`,前缀为 `olive_<module>_`
- 参数和返回值仅使用 C 基础类型、结构体、不透明指针。
- 禁止使用 C++ 特性:类、引用、重载、模板、异常、`std::string``QString`
- 字符串使用 `const char*`UTF-8 编码)。
- 布尔值使用 `int`0 = false,非 0 = true)。
### 1.3 动态库自身可以用 C++
动态库的实现文件(`.cpp`)内部可以继续使用:
- Qt`QObject`, `QString`, `QList`, 信号槽等)
- C++ STL
- 虚函数、模板、Lambda
- 异常(但不得穿透 C 接口边界)
C 接口层只是薄薄的封装胶合层。
---
## 2. 命名规范
| 元素 | 规范 | 示例 |
|---|---|---|
| 类型名 | `Olive` + `PascalCase` | `OliveNodeGraph`, `OliveFrame` |
| 函数名 | `olive_<module>_<snake_case>` | `olive_node_graph_create`, `olive_codec_decoder_open` |
| 枚举名 | `Olive<Module><PascalCase>` | `OliveCodecResultOk`, `OliveRenderModeOffline` |
| 常量宏 | `OLIVE_<MODULE>_UPPER_SNAKE` | `OLIVE_NODE_OK`, `OLIVE_CODEC_ERROR_NOT_FOUND` |
| 版本宏 | `OLIVE_<MODULE>_API_VERSION` | `OLIVE_NODE_API_VERSION 1` |
---
## 3. 内存管理约定
### 3.1 谁创建,谁释放
- **库创建的对象**,必须由库的对应 `destroy`/`free` 函数释放。
- **主进程分配并传入的缓冲区**(如 `char*` 参数),由主进程管理,库内部只读或复制。
- **库返回的字符串/缓冲区**,必须使用库提供的 `free` 函数释放,不能用 C 标准 `free()`(因为库的堆和主进程的堆可能是分离的,尤其是在 Windows 上)。
```c
// 正确:库分配,库释放
char* xml = olive_node_graph_save_xml(graph, &len);
// ... 使用 xml ...
olive_core_free(xml); // 使用库提供的释放函数
// 错误:
free(xml); // 危险!堆可能不一致
```
### 3.2 通用释放函数
每个模块提供一个通用释放函数:
```c
void olive_core_free(void* ptr); // 释放字符串/二进制缓冲区
void olive_core_mem_free(void* ptr, size_t size); // 带大小的释放(用于安全擦除)
```
### 3.3 对象生命周期模式
```c
// 模式 ACreate/Destroy(堆分配)
OliveNodeGraph* olive_node_graph_create(void);
void olive_node_graph_destroy(OliveNodeGraph* obj);
// 模式 BInit/Cleanup(栈分配或外部缓冲区)
int olive_frame_init(OliveFrame* frame, int w, int h, OlivePixelFormat fmt);
void olive_frame_cleanup(OliveFrame* frame);
// 模式 CRef/Unref(引用计数)
void olive_frame_ref(OliveFrame* frame);
void olive_frame_unref(OliveFrame* frame);
```
优先使用 **模式 ACreate/Destroy**,因为不透明指针天然适合堆分配。
---
## 4. 错误处理
### 4.1 返回码约定
所有可能失败的函数返回 `int`
```c
#define OLIVE_OK 0 // 成功
#define OLIVE_ERROR_GENERIC -1 // 通用错误
#define OLIVE_ERROR_INVALID -2 // 无效参数
#define OLIVE_ERROR_NOMEM -3 // 内存不足
#define OLIVE_ERROR_NOT_FOUND -4 // 找不到对象/文件
#define OLIVE_ERROR_IO -5 // IO 错误
#define OLIVE_ERROR_CANCELLED -6 // 操作被取消
#define OLIVE_ERROR_UNSUPPORTED -7 // 不支持的操作
```
### 4.2 详细错误信息
提供线程局部的错误信息获取函数:
```c
int olive_core_last_error_code(void);
const char* olive_core_last_error_string(void); // 线程安全,返回静态缓冲区或 TLS
```
实现方式:
```cpp
// .cpp 中
thread_local int g_last_error_code = OLIVE_OK;
thread_local char g_last_error_string[1024];
static void SetError(int code, const char* fmt, ...) {
g_last_error_code = code;
va_list args;
va_start(args, fmt);
vsnprintf(g_last_error_string, sizeof(g_last_error_string), fmt, args);
va_end(args);
}
```
---
## 5. 字符串处理
### 5.1 输入字符串
- 所有 `const char*` 参数均视为 **UTF-8 编码**
- 库内部在边界处转换为 `QString`
```cpp
// 封装层内部
QString qstr = QString::fromUtf8(cstr);
```
### 5.2 输出字符串
- 返回 `char*` 的函数,使用 `olive_core_free()` 释放。
- 如果只需读取而不持有,提供 `const char*` 返回版本:
```c
const char* olive_node_get_type_name(OliveNode* node); // 生命周期与 node 绑定
char* olive_node_graph_save_xml(OliveNodeGraph* graph, size_t* out_len); // 需释放
```
---
## 6. 显式加载器(ModuleLoader
### 6.1 设计目标
主进程通过一个统一的 `ModuleLoader` 类显式加载所有动态库,将 `dlopen`/`dlsym` 的细节隐藏。
### 6.2 C++ 封装类
```cpp
// app/moduleloader.h
#pragma once
#include <QString>
#include <QHash>
#include <functional>
namespace olive {
class ModuleLoader {
public:
ModuleLoader();
~ModuleLoader();
// 加载指定路径的动态库
bool Load(const QString& module_name, const QString& library_path);
// 卸载
void Unload(const QString& module_name);
// 获取函数指针(模板封装,内部调用 dlsym)
template<typename FuncPtr>
FuncPtr GetFunction(const QString& module_name, const char* func_name) {
return reinterpret_cast<FuncPtr>(GetFunctionRaw(module_name, func_name));
}
// 检查是否已加载
bool IsLoaded(const QString& module_name) const;
// 获取加载错误信息
QString LastError() const;
private:
void* GetFunctionRaw(const QString& module_name, const char* func_name);
struct ModuleHandle {
void* handle; // dlopen handle
QString path;
};
QHash<QString, ModuleHandle> modules_;
QString last_error_;
};
// 便捷宏:从指定模块获取函数并调用
#define OLIVE_LOAD_FUNC(loader, module, name, type) \
auto name = (loader).GetFunction<type>(module, #name); \
if (!name) { qFatal("Failed to load function: " #name " from module: " #module); }
} // namespace olive
```
### 6.3 实现(POSIX
```cpp
// app/moduleloader.cpp
#include "moduleloader.h"
#include <dlfcn.h>
#include <QDebug>
namespace olive {
bool ModuleLoader::Load(const QString& module_name, const QString& library_path) {
if (modules_.contains(module_name)) return true;
void* handle = dlopen(library_path.toUtf8().constData(), RTLD_NOW | RTLD_LOCAL);
if (!handle) {
last_error_ = QString::fromUtf8(dlerror());
qWarning() << "Failed to load" << library_path << ":" << last_error_;
return false;
}
modules_.insert(module_name, {handle, library_path});
qInfo() << "Loaded module:" << module_name << "from" << library_path;
return true;
}
void ModuleLoader::Unload(const QString& module_name) {
auto it = modules_.find(module_name);
if (it != modules_.end()) {
dlclose(it->handle);
modules_.erase(it);
}
}
void* ModuleLoader::GetFunctionRaw(const QString& module_name, const char* func_name) {
auto it = modules_.find(module_name);
if (it == modules_.end()) return nullptr;
dlerror(); // 清除之前的错误
void* func = dlsym(it->handle, func_name);
return func;
}
} // namespace olive
```
### 6.4 实现(Windows
```cpp
#include <windows.h>
bool ModuleLoader::Load(const QString& module_name, const QString& library_path) {
HMODULE handle = LoadLibraryW(library_path.toStdWString().c_str());
if (!handle) {
last_error_ = QString::number(GetLastError());
return false;
}
modules_.insert(module_name, {handle, library_path});
return true;
}
void* ModuleLoader::GetFunctionRaw(const QString& module_name, const char* func_name) {
auto it = modules_.find(module_name);
if (it == modules_.end()) return nullptr;
return GetProcAddress(static_cast<HMODULE>(it->handle), func_name);
}
```
### 6.5 使用示例
```cpp
// core.cpp 中初始化
module_loader_ = new ModuleLoader();
module_loader_->Load("core", FindLibraryPath("libolivecore.so"));
module_loader_->Load("codec", FindLibraryPath("libolivecodec.so"));
module_loader_->Load("node", FindLibraryPath("libolivenode.so"));
// 获取函数
OLIVE_LOAD_FUNC(*module_loader_, "core", olive_rational_make, OliveRational(*)(int64_t, int64_t));
OliveRational r = olive_rational_make(1001, 30000);
```
---
## 7. 类型映射表
| C++ 类型(内部) | C 接口类型(公共) | 说明 |
|---|---|---|
| `olive::Rational` | `OliveRational` | `struct { int64_t num, den; }` |
| `olive::Color` | `OliveColor` | `struct { double r, g, b, a; }` |
| `olive::TimeRange` | `OliveTimeRange*` | 不透明指针 |
| `olive::Frame` | `OliveFrame*` | 不透明指针 |
| `olive::SampleBuffer` | `OliveSampleBuffer*` | 不透明指针 |
| `olive::PixelFormat` | `OlivePixelFormat` | `enum` |
| `olive::VideoParams` | `OliveVideoParams` | 公开结构体(POD |
| `olive::AudioParams` | `OliveAudioParams` | 公开结构体(POD |
| `olive::Node*` | `OliveNode*` | 不透明指针 |
| `olive::NodeGraph*` | `OliveNodeGraph*` | 不透明指针 |
| `olive::RenderTicketPtr` | `OliveRenderTicket*` | 不透明指针(引用计数内部管理) |
| `QString` | `const char*` | UTF-8 编码 |
| `QSize` | `struct { int width; int height; }` | `OliveSize` |
| `QMatrix4x4` | `float[16]` | 列优先 |
### 7.1 POD 结构体定义示例
```c
// olivecore_api.h
typedef struct {
int64_t num;
int64_t den;
} OliveRational;
typedef struct {
double r;
double g;
double b;
double a;
} OliveColor;
typedef struct {
int width;
int height;
int depth;
int channel_count;
OlivePixelFormat format;
double pixel_aspect_num;
double pixel_aspect_den;
} OliveVideoParams;
typedef struct {
int sample_rate;
int64_t channel_layout; // FFmpeg AV_CH_LAYOUT_* 值
OliveSampleFormat format;
} OliveAudioParams;
typedef struct {
int width;
int height;
} OliveSize;
```
---
## 8. 线程安全
### 8.1 API 层面
- **默认不保证线程安全**。除非文档明确标注 `thread-safe`,否则每个 `OliveXxx*` 对象只能在创建它的线程中使用。
- 这是刻意的设计:由于渲染进程是"用完即弃"的,不存在多线程共享渲染状态的问题。
### 8.2 主进程中的线程使用
- `ModuleLoader` 本身是线程安全的(只读查找,加载/卸载在初始化/退出时串行执行)。
- UI 对象在主线程操作。
- IO/解码可以在工作线程中通过 C API 操作独立的 `OliveDecoder*` 实例。
---
## 9. 版本与 ABI 兼容性
### 9.1 API 版本号
每个模块的 C API 有一个主版本号:
```c
#define OLIVE_NODE_API_VERSION 1
int olive_node_api_version(void); // 返回 OLIVE_NODE_API_VERSION
```
### 9.2 加载时版本检查
```cpp
bool LoadNodeModule(ModuleLoader* loader, const QString& path) {
if (!loader->Load("node", path)) return false;
auto version_fn = loader->GetFunction<int(*)()>("node", "olive_node_api_version");
if (!version_fn || version_fn() != EXPECTED_NODE_API_VERSION) {
qFatal("Incompatible libolivenode.so version");
return false;
}
return true;
}
```
### 9.3 ABI 兼容性规则
- **允许**:新增函数、新增枚举值(在末尾)、新增结构体字段(在末尾,且文档标注"v2 起可用")。
- **不允许**:删除函数、修改函数签名、修改已有字段含义、改变枚举值顺序。
- **结构体扩展**:POD 结构体新增字段时,提供初始化宏确保旧代码不会未初始化新字段:
```c
#define OLIVE_VIDEO_PARAMS_DEFAULT { \
.width = 1920, .height = 1080, .depth = 1, \
.channel_count = 4, .format = OLIVE_PIXEL_FMT_RGBA32F, \
.pixel_aspect_num = 1.0, .pixel_aspect_den = 1.0 \
}
```
---
## 10. 头文件组织
### 10.1 目录结构
```
c_api/
├── include/
│ ├── olive/ # 公共 C API 头文件(安装时发布)
│ │ ├── core_api.h
│ │ ├── codec_api.h
│ │ ├── node_api.h
│ │ ├── render_api.h
│ │ ├── audio_api.h
│ │ ├── plugin_api.h
│ │ ├── ui_api.h
│ │ └── olive_api.h # 总入口,包含所有模块
│ └── olivecpp/ # 主进程内部使用的 C++ 辅助封装
│ ├── module_loader.h
│ ├── core_wrapper.h // RAII 包装类
│ ├── node_wrapper.h
│ └── ...
└── src/
├── core_api.cpp // 对应各模块的 C 封装实现
├── codec_api.cpp
├── node_api.cpp
├── render_api.cpp
├── audio_api.cpp
├── plugin_api.cpp
└── ui_api.cpp
```
### 10.2 C API 头文件示例
```c
// c_api/include/olive/node_api.h
#ifndef OLIVE_NODE_API_H
#define OLIVE_NODE_API_H
#ifdef __cplusplus
extern "C" {
#endif
#include "core_api.h"
#define OLIVE_NODE_API_VERSION 1
/* ========== 类型前向声明 ========== */
typedef struct OliveNodeGraph OliveNodeGraph;
typedef struct OliveNode OliveNode;
typedef struct OliveParam OliveParam;
/* ========== 函数导出宏 ========== */
#ifdef OLIVE_BUILDING_NODE
# define OLIVE_NODE_API __attribute__((visibility("default")))
#else
# define OLIVE_NODE_API
#endif
/* ========== API 版本 ========== */
OLIVE_NODE_API int olive_node_api_version(void);
/* ========== NodeGraph ========== */
OLIVE_NODE_API OliveNodeGraph* olive_node_graph_create(void);
OLIVE_NODE_API void olive_node_graph_destroy(OliveNodeGraph* g);
OLIVE_NODE_API int olive_node_graph_load_xml(OliveNodeGraph* g,
const char* xml_data,
size_t xml_len);
OLIVE_NODE_API char* olive_node_graph_save_xml(OliveNodeGraph* g,
size_t* out_len);
OLIVE_NODE_API OliveNode* olive_node_graph_find_node(OliveNodeGraph* g,
const char* node_id);
OLIVE_NODE_API int olive_node_graph_add_node(OliveNodeGraph* g,
const char* node_type,
const char* node_id);
/* ========== Node ========== */
OLIVE_NODE_API const char* olive_node_get_id(OliveNode* node);
OLIVE_NODE_API const char* olive_node_get_type_name(OliveNode* node);
OLIVE_NODE_API int olive_node_connect(OliveNode* from_node,
int from_output_index,
OliveNode* to_node,
int to_input_index);
/* ========== Param ========== */
OLIVE_NODE_API int olive_node_set_param_int(OliveNode* node,
const char* param_name,
int64_t value);
OLIVE_NODE_API int olive_node_set_param_double(OliveNode* node,
const char* param_name,
double value);
OLIVE_NODE_API int olive_node_set_param_rational(OliveNode* node,
const char* param_name,
OliveRational value);
OLIVE_NODE_API int olive_node_set_param_string(OliveNode* node,
const char* param_name,
const char* value);
/* ========== Project ========== */
OLIVE_NODE_API OliveNodeGraph* olive_project_create(const char* name);
OLIVE_NODE_API int olive_project_load_file(OliveNodeGraph* project,
const char* filename);
OLIVE_NODE_API int olive_project_save_file(OliveNodeGraph* project,
const char* filename);
#ifdef __cplusplus
}
#endif
#endif // OLIVE_NODE_API_H
```
---
## 11. CMake 中的 C API 编译
### 11.1 为模块添加 C API 目标
```cmake
# app/node/CMakeLists.txt
# 原有 C++ 源码(内部实现,不暴露头文件)
set(NODE_INTERNAL_SOURCES
node.cpp node.h
traverser.cpp traverser.h
project/project.cpp project/project.h
# ... 其他内部文件
)
# C API 封装层源码
set(NODE_API_SOURCES
${CMAKE_SOURCE_DIR}/c_api/src/node_api.cpp
)
# 模块对外头文件(安装时发布)
set(NODE_API_HEADERS
${CMAKE_SOURCE_DIR}/c_api/include/olive/node_api.h
)
# 创建动态库
add_library(olivenode SHARED
${NODE_INTERNAL_SOURCES}
${NODE_API_SOURCES}
)
target_compile_definitions(olivenode PRIVATE OLIVE_BUILDING_NODE)
target_include_directories(olivenode
PRIVATE
${CMAKE_SOURCE_DIR}/app
${CMAKE_SOURCE_DIR}/c_api/include
PUBLIC
$<INSTALL_INTERFACE:include>
)
target_link_libraries(olivenode
PRIVATE
olivecore
olivecodec
Qt${QT_VERSION_MAJOR}::Core
)
# 设置符号可见性:默认隐藏,只有标记 OLIVE_NODE_API 的才导出
set_target_properties(olivenode PROPERTIES
CXX_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN YES
)
# 安装 C API 头文件
install(FILES ${NODE_API_HEADERS} DESTINATION include/olive)
install(TARGETS olivenode DESTINATION lib)
```
### 11.2 主可执行文件不链接业务库
```cmake
# app/CMakeLists.txt(改造后)
add_executable(olive-editor
main.cpp
core.cpp
core.h
${CMAKE_SOURCE_DIR}/c_api/src/module_loader.cpp # 显式加载器实现
)
# 主程序只链接 Qt 和系统库,不链接 olivecore/olivecodec 等业务库!
target_link_libraries(olive-editor PRIVATE
Qt${QT_VERSION_MAJOR}::Core
Qt${QT_VERSION_MAJOR}::Gui
Qt${QT_VERSION_MAJOR}::Widgets
# ... 其他 UI 依赖
)
target_include_directories(olive-editor PRIVATE
${CMAKE_SOURCE_DIR}/c_api/include
)
```
---
## 12. 测试策略
### 12.1 C API 单元测试
为每个 C API 函数编写独立测试:
```cpp
// tests/c_api/test_node_api.cpp
#include <gtest/gtest.h>
#include "olive/node_api.h"
TEST(NodeAPITest, CreateDestroy) {
OliveNodeGraph* g = olive_node_graph_create();
ASSERT_NE(g, nullptr);
olive_node_graph_destroy(g);
}
TEST(NodeAPITest, AddNodeAndParam) {
OliveNodeGraph* g = olive_node_graph_create();
ASSERT_EQ(OLIVE_OK, olive_node_graph_add_node(g, "Transform", "T1"));
OliveNode* n = olive_node_graph_find_node(g, "T1");
ASSERT_NE(n, nullptr);
ASSERT_EQ(OLIVE_OK, olive_node_set_param_double(n, "position_x", 100.0));
olive_node_graph_destroy(g);
}
```
### 12.2 ABI 稳定性测试
在 CI 中:
1. 编译当前版本的动态库。
2. 用上一个发布版本的测试可执行文件加载当前动态库运行。
3. 验证所有测试通过(确保未破坏 ABI)。
---
## 13. 常见陷阱
| 陷阱 | 说明 | 对策 |
|---|---|---|
| **异常穿透 C 边界** | C++ 异常抛出到 C 调用方是 UB。 | 所有 C API 函数用 `try/catch(...)` 包裹,捕获所有异常并转换为错误码。 |
| **RTTI 跨边界** | `dynamic_cast` 在不同动态库间可能失败。 | C 接口不使用 RTTI,内部若必须 `dynamic_cast`,确保类型定义在同一个库内。 |
| **Qt 元对象跨库** | `qobject_cast` 依赖 moc 生成的静态元对象数据,跨库时可能失效。 | 不在 C API 中暴露 Qt 对象,所有 Qt 对象封装在库内部。 |
| **全局静态变量** | 多个动态库各有一份全局静态变量。 | 避免在 C API 头文件中定义全局静态变量,使用函数内 static + 首次调用初始化。 |
| **堆不一致(Windows** | A 库 `malloc`B 库 `free` 导致崩溃。 | 严格遵循"谁分配谁释放",使用库提供的 `olive_core_free()`。 |
@@ -1,345 +0,0 @@
# 10 周小步快跑实施路线图
> 本路线图为**渐进式、可回退、可并行**的实施计划。每一周都有明确的交付物和验收标准。任何一周的任务如果超时或遇到阻碍,都可以独立回退或跳过,不影响其他周的进度。
---
## 关键原则
1. **不改现有代码,只增代码**:每一周的改造都通过新增文件(`c_api/``app/render/renderer_main.cpp` 等)完成,现有源文件尽量不动。只有必须解耦的地方才修改现有头文件。
2. **编译开关控制**:通过 CMake 选项 `-DOLIVE_DYNAMIC_MODULES=ON` 控制是否走动态库路径。默认 OFF,确保主干始终可编译可运行。
3. **每周可独立验证**:每周结束都有一个可运行的版本,即使后续周不开始,当前成果也是有价值的。
4. **先易后难**:从耦合最低的模块(core、codec)开始,积累经验和工具链,最后攻克最难的 node/render。
---
## 人员分工建议(假设 2–3 人)
| 角色 | 负责内容 |
|---|---|
| **基础设施工程师** | ModuleLoader、CMake 改造、CI 适配、C API 规范执行 |
| **编解码工程师** | `libolivecore.so``libolivecodec.so``liboliveaudio.so` |
| **渲染工程师** | `libolivenode.so` 解耦、`liboliverender.so``olive-renderer` 多进程 |
| **UI 工程师** | `liboliveui.so`、主进程集成、ViewerWidget 改造 |
**注意**:初期阶段(Week 1–3)只需 1 人负责基础设施 + 1 人负责编解码即可。渲染和 UI 改造在 Week 4 之后全面展开。
---
## Week 1:基础设施与工具链(1 人主责)
### 目标
建立 C API 基础设施,验证显式加载工具链在所有目标平台正常工作。
### 任务清单
- [ ] **T1.1** 创建目录结构 `c_api/include/olive/`, `c_api/src/`, `c_api/tests/`
- [ ] **T1.2** 编写 `ModuleLoader` 类(`c_api/src/module_loader.h/cpp`),支持 POSIX + Windows。
- [ ] **T1.3** 定义全局 C API 规范文件:`c_api/include/olive/core_api.h` 的基础部分(`OliveResult`, `OliveRational`, 内存管理函数)。
- [ ] **T1.4** 编写最小测试动态库 `c_api/tests/test_module/`(只导出一个 `int test_add(int, int)`),验证 `ModuleLoader` 可以正确加载和调用。
- [ ] **T1.5** 在 CMake 中新增 `OLIVE_DYNAMIC_MODULES` 选项(默认 OFF)。
- [ ] **T1.6** 在 CIGitHub Actions)中增加一个 job:开启 `OLIVE_DYNAMIC_MODULES=ON` 编译,验证 Linux/macOS/Windows 三平台。
### 验收标准
```bash
# 运行测试
./tests/c_api/test_module_loader
# 输出:
# [PASS] Load test_module.so
# [PASS] Call test_add(2, 3) = 5
# [PASS] Unload test_module.so
```
### 回退策略
`ModuleLoader` 在某平台工作异常,该周可仅完成 POSIX 平台,Windows 平台延后处理。
---
## Week 2libolivecore.so1 人主责)
### 目标
`ext/core/` 从静态库改造为显式加载的动态库,建立第一个完整的 C API。
### 任务清单
- [ ] **T2.1** 修改 `ext/core/CMakeLists.txt``add_library(olivecore SHARED ...)`,添加 `CXX_VISIBILITY_PRESET hidden`
- [ ] **T2.2**`ext/core/` 中需要跨库使用的类添加导出宏(或保持 C API 头文件中的 `OLIVE_CORE_API`)。
- [ ] **T2.3** 完成 `c_api/include/olive/core_api.h`Rational, Color, TimeRange, Timecode, PixelFormat, SampleBuffer, VideoParams, AudioParams。
- [ ] **T2.4** 编写 `c_api/src/core_api.cpp`,封装所有上述类型。
- [ ] **T2.5** 编写单元测试 `tests/c_api/test_core_api.cpp`
- [ ] **T2.6** 在主进程 `Core::Start()` 中尝试显式加载 `libolivecore.so`,失败时打印警告但不阻塞启动。
### 验收标准
```cpp
ModuleLoader loader;
loader.Load("core", "./libolivecore.so");
auto make = loader.GetFunction<OliveRational(*)(int64_t,int64_t)>("core", "olive_rational_make");
auto add = loader.GetFunction<OliveRational(*)(OliveRational,OliveRational)>("core", "olive_rational_add");
OliveRational r = add(make(1,2), make(1,3));
assert(r.num == 5 && r.den == 6);
```
### 回退策略
若 C API 封装工作量超预期,本周可只完成 Rational + TimeRange 的最小子集,其余类型后续补充。
---
## Week 3libolivecodec.so1 人主责)
### 目标
`app/codec/` + `app/common/` 封装为显式加载动态库,实现解码器的 C API。
### 任务清单
- [ ] **T3.1**`app/common/` 的源文件并入 `app/codec/CMakeLists.txt`
- [ ] **T3.2** 创建 `libolivecodec.so` 的 SHARED 目标。
- [ ] **T3.3** 编写 `c_api/include/olive/codec_api.h`(最小子集):MediaInfo, Decoder, Frame。
- [ ] **T3.4** 编写 `c_api/src/codec_api.cpp`
- [ ] **T3.5** 编写测试:加载视频文件 → 解码第一帧 → 验证宽高 > 0。
- [ ] **T3.6** 在主进程中显式加载 `libolivecodec.so`
### 验收标准
```cpp
auto decoder = olive_decoder_create(nullptr);
olive_decoder_open(decoder, "test.mp4", 0);
OliveFrame* frame = nullptr;
olive_decoder_decode_video(decoder, olive_rational_make(0,1), &frame);
assert(olive_frame_width(frame) == 1920);
olive_frame_destroy(frame);
olive_decoder_destroy(decoder);
```
### 回退策略
`common/` 中有代码依赖 `node/``render/`,先将这些代码移回主库,再继续。
---
## Week 4libolivenode.so 解耦(2 人并行,最关键的一周)
### 目标
解决 `Node.h``render/` 的头文件依赖,为 `libolivenode.so` 的独立编译扫清障碍。
### 任务清单
- [ ] **T4.1** 创建 `app/node/nodecachecallbacks.h`,定义 `NodeCacheCallbacks` 纯虚接口。
- [ ] **T4.2** 修改 `app/node/node.h`
- 移除 `#include "render/rendercache.h"`
- 添加 `NodeCacheCallbacks* cache_callbacks_``SetCacheCallbacks()`
-`InvalidateCache` 相关逻辑改为调用 `cache_callbacks_->InvalidateCache()`
- [ ] **T4.3** 创建 `app/node/jobtypes.h`,定义 `NodeJobType``NodeJobData`
- [ ] **T4.4** 修改 `Node.h` 中的 `ProcessXxx` 虚函数签名,使用 `NodeJobData`
- [ ] **T4.5** 修改 `RenderProcessor`,适配新的 `NodeCacheCallbacks``NodeJobData`
- [ ] **T4.6** 验证 `app/node/` 目录可以独立编译(写一个临时 CMake 测试)。
### 验收标准
```bash
cd /tmp && cmake /path/to/oak/app/node && make
# 编译成功,不报错
```
### 回退策略
**若解耦工作量超预期**:允许 `node/``render/` 暂时合并为 `libolive-engine.so`。这是最重要的回退策略——宁可合并也不阻塞进度。合并后仍可继续封装 C API,后续再拆分。
---
## Week 5libolivenode.so C API + liboliveplugin.so2 人并行)
### 目标
完成节点图系统的 C API,并将 OFX 插件宿主独立为动态库。
### 任务清单(节点图工程师)
- [ ] **T5.1** 创建 `libolivenode.so`,聚合 `node/`, `timeline/`, `undo/`, `config/`
- [ ] **T5.2** 编写 `c_api/include/olive/node_api.h``c_api/src/node_api.cpp`
- [ ] **T5.3** 实现最小 C APINodeGraph create/destroy, load_xml/save_xml, add_node, connect, param set/get。
- [ ] **T5.4** 编写测试:用 C API 构建一个 Generator -> ViewerOutput 的图,序列化后反序列化验证。
### 任务清单(插件工程师)
- [ ] **T5.5**`pluginSupport/` 从主 OBJECT 库移出,创建 `liboliveplugin.so`
- [ ] **T5.6** 编写 `c_api/include/olive/plugin_api.h`(最小子集):host create/destroy, add_path, rescan, plugin count/get。
- [ ] **T5.7** 验证主进程可以扫描 OFX 插件目录并列出插件名称。
### 验收标准
```cpp
// 节点图测试
OliveNodeGraph* g = olive_node_graph_create();
olive_node_graph_add_node(g, "SolidGenerator", "Solid1");
olive_node_graph_add_node(g, "ViewerOutput", "Viewer1");
OliveNode* solid = olive_node_graph_find_node(g, "Solid1");
OliveNode* viewer = olive_node_graph_find_node(g, "Viewer1");
olive_node_connect(solid, 0, viewer, 0);
size_t len;
char* xml = olive_node_graph_save_xml(g, &len);
assert(len > 0);
olive_core_free(xml);
```
### 回退策略
`node/` C API 工作量过大,优先保证 `load_xml` / `save_xml` / `find_node` 三个函数(这是渲染子进程最需要的),其余延后。
---
## Week 6olive-renderer 单帧端到端(2 人并行)
### 目标
实现第一个可用的 `olive-renderer` 可执行文件,能渲染一帧测试图。
### 任务清单
- [ ] **T6.1** 编写 `app/render/renderer_main.cpp`,实现命令行解析。
- [ ] **T6.2** 实现 `olive_shm_create/open/close/unlink`POSIX + Windows)。
- [ ] **T6.3**`olive-renderer` 中集成:加载 XML → 初始化 OpenGL → 渲染 → 写入 SHM → 输出 JSON。
- [ ] **T6.4** 编写主进程中的 `RenderProcessLauncher::RenderFrameSync`
- [ ] **T6.5** 端到端测试:主进程启动 `olive-renderer` 渲染一帧纯色,验证 SHM 中的像素值正确。
### 验收标准
```bash
# 命令行直接测试
olive-renderer --mode=frame --node-graph=test_solid.xml --time=0/1 \
--video-params='{"width":100,"height":100,"format":"rgba32f"}' \
--output-shm=/olive_test --output-shm-size=160000
# 输出:
# {"status":"ok","width":100,"height":100,"format":"rgba32f",...}
```
### 回退策略
若 OpenGL 离屏上下文初始化在某平台失败,该平台暂时使用 `--backend=dummy`(只测试进程模型,不测试实际渲染)。
---
## Week 7ViewerWidget 集成 + 用完即弃验证(2 人并行)
### 目标
`olive-renderer` 集成到主进程的 Viewer 中,验证"用完即弃"模型在实际场景中的可行性。
### 任务清单
- [ ] **T7.1** 修改 `ViewerWidget`:从 `RenderManager::RenderFrame()` 改为启动 `olive-renderer`
- [ ] **T7.2** 实现异步完成回调:`OnRenderProcessFinished()` 读取 SHM 并更新 Texture。
- [ ] **T7.3** 处理子进程崩溃:崩溃时忽略该帧,保持上一帧显示,记录日志。
- [ ] **T7.4** 测量实际性能:拖动时间线时的帧率、CPU 占用、进程启动耗时。
- [ ] **T7.5** 若性能不达标,实现批处理模式(`--mode=batch`)。
### 验收标准
- 打开一个简单项目(单轨道 + 纯色生成器),拖动时间线,Viewer 实时更新。
- `kill -9` 一个渲染子进程,主进程不崩溃,Viewer 保持显示。
### 回退策略
若"用完即弃"性能完全不可接受(如帧率 < 5fps),立即切换为**批处理模式**或**进程池模式**(预启动 N 个进程,循环使用,每个进程渲染一批后自杀)。
---
## Week 8liboliveaudio.so + liboliveui.so2 人并行)
### 目标
完成音频库和 UI 库的动态库拆分。
### 任务清单(音频工程师)
- [ ] **T8.1** 创建 `liboliveaudio.so`
- [ ] **T8.2** 编写 `audio_api.h/cpp`(最小子集):manager init/play/pause/push_buffer。
- [ ] **T8.3** 验证音频播放通过 C API 正常工作。
### 任务清单(UI 工程师)
- [ ] **T8.1** 聚合所有 UI 源文件,创建 `liboliveui.so`
- [ ] **T8.2** 解耦 `Core` 类中的 UI 逻辑(`StartGUI()` 迁移到 `liboliveui.so` 内部)。
- [ ] **T8.3** 主进程显式加载 `liboliveui.so`,成功启动主窗口。
- [ ] **T8.4** 编写 `ui_api.h`(最小子集):app create/exec, main_window show, open_project。
### 验收标准
- 主程序通过显式加载 `liboliveui.so` 启动,界面正常。
- 音频播放正常(可听到声音)。
---
## Week 9:导出集成 + 稳定性打磨(2 人并行)
### 目标
将导出流程集成到多进程渲染模型,全面稳定性测试。
### 任务清单
- [ ] **T9.1** 修改导出任务(`task/export/`),使用 `olive-renderer` 逐帧/逐批渲染。
- [ ] **T9.2** 导出天然适合批处理:一次性发送 10–50 帧给子进程。
- [ ] **T9.3** 编写压力测试:连续渲染 100 帧,验证无内存泄漏、无共享内存泄漏。
- [ ] **T9.4** 测试 OFX 插件在子进程中的渲染(选择几个免费 OFX 插件测试)。
- [ ] **T9.5** 测试崩溃场景:`kill -9` 随机子进程,验证主进程稳定。
### 验收标准
- 成功导出一个 10 秒视频(300 帧),画面正确。
- 连续启动 100 个渲染子进程,系统无共享内存泄漏(`ls /dev/shm/` 检查)。
---
## Week 10:打包适配 + 文档 + 性能优化(2 人并行)
### 目标
完成打包脚本适配,编写用户文档,进行最终性能优化。
### 任务清单
- [ ] **T10.1** 更新 macOS 打包脚本:确保 `libolive*.dylib``olive-renderer` 被打入 `.app` Bundle`@rpath` 设置正确。
- [ ] **T10.2** 更新 Windows 打包脚本:确保 `.dll``olive-renderer.exe` 在_installer 中。
- [ ] **T10.3** 更新 Linux AppImage 打包:确保动态库在 AppImage 内可加载。
- [ ] **T10.4** 更新 `docs/build.md``docs/build-macos-zh.md`,说明新的运行时依赖。
- [ ] **T10.5** 性能优化:
- 节点图 XML 缓存(相同图只序列化一次)。
- 共享内存预分配池(避免反复创建/销毁)。
- 批处理大小动态调整(根据上一批的渲染时间调整下一批的帧数)。
- [ ] **T10.6** 全面回归测试:导入、编辑、预览、导出、Undo/Redo、保存/加载项目。
### 验收标准
- 在三平台上都能通过 `make install` 或打包脚本生成可分发包。
- 新用户按照 `build.md` 可以成功编译并运行。
- 与 Week 0(改造前)相比,导出速度不差于 90%,预览帧率不差于 70%。
---
## 并行工作流
```
Week 1: [基础设施]
Week 2: [core] (依赖 W1)
Week 3: [codec] (依赖 W2)
Week 4: [node 解耦] (可并行 W3, 但建议 W3 后启动)
Week 5: [node C API] + [plugin] (依赖 W4)
Week 6: [renderer] (依赖 W5)
Week 7: [Viewer 集成] (依赖 W6)
Week 8: [audio] + [ui] (可并行 W7)
Week 9: [导出集成] (依赖 W7)
Week 10: [打包/文档/优化] (依赖 W9)
```
**最大并行度**:Week 8 时可以有 3 人同时工作(1 人 audio1 人 ui1 人优化 renderer)。
---
## 回退总策略
| 场景 | 回退方案 |
|---|---|
| 某周任务无法按时完成 | 将该周剩余任务移到下一周,当前周只交付已完成部分。 |
| `node/` 解耦完全不可行 | 将 `node/` + `render/` 合并为 `libolive-engine.so`,后续再拆分。 |
| "用完即弃"性能完全不可接受 | 切换为"批处理模式"(每进程渲染 N 帧),或"进程池模式"(预启动 N 个进程)。 |
| 动态库在某平台加载失败 | 该平台暂时保持静态链接,其他平台先用动态库。 |
| C API 维护成本过高 | 保留 C API 用于子进程通信,主进程内部恢复直接 C++ 链接(但库仍编译为动态库,由操作系统隐式加载)。 |
| 项目期限紧张 | 优先完成 `olive-renderer` 多进程(核心价值),动态库拆分可以延后。 |
@@ -1,479 +0,0 @@
# IPC 协议规范
> 本文件定义主进程与 `olive-renderer` 子进程之间的全部通信方式。由于采用**"用完即弃"**模型,协议被设计为**极简、无状态、单向请求-响应**。
---
## 1. 通信通道概览
| 通道 | 方向 | 用途 | 格式 |
|---|---|---|---|
| **命令行参数** | 主进程 → 子进程 | 传递渲染配置(模式、路径、参数) | POSIX 风格长选项 |
| **stdin** | 主进程 → 子进程 | 传递节点图 XML(当 XML 过大超出命令行长度限制时) | 原始 XML 字符串 |
| **stdout** | 子进程 → 主进程 | 返回渲染结果元数据 | 单行 JSON |
| **stderr** | 子进程 → 主进程 | 日志和详细错误信息 | 纯文本 |
| **共享内存 / 内存映射文件** | 双向 | 传输大帧数据(像素/采样) | 二进制(ShmHeader + 原始数据) |
| **退出码** | 子进程 → 主进程 | 快速判断成功/失败/异常 | 整数 (0–255) |
**重要**:由于子进程渲染完成后立即退出,不存在**长期状态同步**、**心跳**、**取消信号**(主进程直接 `kill` 即可)。
---
## 2. 命令行参数
### 2.1 参数总表
| 参数 | 类型 | 必需 | 默认值 | 说明 |
|---|---|---|---|---|
| `--mode` | string | 否 | `frame` | 渲染模式:`frame`, `batch`, `audio` |
| `--node-graph` | path | 是 | — | 节点图 XML 文件路径 |
| `--node-graph-stdin` | flag | 否 | false | 从 stdin 读取节点图 XML,而非文件 |
| `--output-node` | string | 否 | 自动探测 | 输出节点 ID |
| `--time` | rational | 条件 | — | 单帧时间点(`--mode=frame` 时必需) |
| `--times` | csv | 条件 | — | 多帧时间点列表(`--mode=batch` 时必需) |
| `--start` | rational | 条件 | — | 音频起始时间(`--mode=audio` 时必需) |
| `--duration` | rational | 条件 | — | 音频持续时间(`--mode=audio` 时必需) |
| `--video-params` | json | 条件 | — | 视频参数(`--mode=frame`/`batch` 时必需) |
| `--audio-params` | json | 条件 | — | 音频参数(`--mode=audio` 时必需) |
| `--color-ref` | string | 否 | — | 参考色彩空间名称 |
| `--color-display` | string | 否 | — | 显示色彩空间名称 |
| `--force-size` | json | 否 | — | 强制输出尺寸,如 `{"width":1920,"height":1080}` |
| `--force-format` | string | 否 | — | 强制像素格式,如 `rgba32f` |
| `--output-shm` | string | 是 | — | 输出共享内存名称或临时文件路径 |
| `--output-shm-size` | int | 是 | — | 输出缓冲区大小(字节) |
| `--output-stdout` | flag | 否 | false | 将帧数据 base64 编码输出到 stdout(仅小帧/测试) |
| `--backend` | string | 否 | `opengl` | 渲染后端:`opengl`, `dummy` |
| `--shader-path` | path | 否 | `<exe_dir>/shaders` | 着色器资源目录 |
| `--ocio-config` | path | 否 | — | OCIO 配置文件路径 |
| `--verbose` | flag | 否 | false | 详细日志输出到 stderr |
| `--version` | flag | 否 | false | 输出版本信息并退出 |
| `--help` | flag | 否 | false | 输出帮助信息并退出 |
### 2.2 参数值格式
**Rational**`"<numerator>/<denominator>"`,如 `"1001/30000"`, `"0/1"`
**JSON**:紧凑格式,键用双引号。例如:
```
--video-params='{"width":1920,"height":1080,"format":"rgba32f","channel_count":4,"depth":1}'
```
**CSV**:逗号分隔的有理数字符串。例如:
```
--times="0/24,1/24,2/24,3/24,4/24"
```
---
## 3. 标准输入(stdin
`--node-graph-stdin` 标志存在时,子进程从 stdin 读取节点图 XML,而不是从 `--node-graph` 指定的文件。
### 3.1 使用场景
- 节点图 XML 非常大(> 100KB),超出命令行长度限制。
- 主进程不想在磁盘上创建临时文件。
- 安全考虑:敏感项目数据不写入磁盘。
### 3.2 协议
```
主进程 子进程
│ │
│── XML 数据 ──>│(子进程读取 stdin 直到 EOF)
│ │
```
子进程读取 stdin 的全部内容,视为节点图 XML 字符串。XML 结束后不需要特殊分隔符(EOF 即结束)。
**注意**:由于子进程使用 `QCoreApplication` 且不使用 Qt 的事件循环读取 stdin,应使用阻塞式 `QTextStream(stdin).readAll()``std::cin`
---
## 4. 标准输出(stdout
子进程将渲染结果以**单行 JSON** 输出到 stdout,以换行符 `\n` 结尾。主进程读取第一行后即视为响应完成。
### 4.1 单帧模式输出(`--mode=frame`
**成功:**
```json
{
"status": "ok",
"mode": "frame",
"time": "1001/30000",
"width": 1920,
"height": 1080,
"format": "rgba32f",
"pixel_format_id": 28,
"channel_count": 4,
"shm_name": "/olive_frame_abc123",
"data_offset": 256,
"data_size": 33177600,
"linesize": 7680,
"render_time_ms": 42
}
```
**失败:**
```json
{
"status": "error",
"mode": "frame",
"time": "1001/30000",
"error_code": "decoder_failure",
"message": "Failed to open decoder for footage 'clip001.mp4'",
"render_time_ms": 5
}
```
### 4.2 批处理模式输出(`--mode=batch`
**成功:**
```json
{
"status": "ok",
"mode": "batch",
"frame_count": 5,
"frames": [
{"time": "0/24", "data_offset": 256, "data_size": 33177600, "render_time_ms": 45},
{"time": "1/24", "data_offset": 33178056, "data_size": 33177600, "render_time_ms": 38},
{"time": "2/24", "data_offset": 66356112, "data_size": 33177600, "render_time_ms": 41},
{"time": "3/24", "data_offset": 99534168, "data_size": 33177600, "render_time_ms": 39},
{"time": "4/24", "data_offset": 132712224, "data_size": 33177600, "render_time_ms": 42}
],
"total_render_time_ms": 205
}
```
### 4.3 音频模式输出(`--mode=audio`
**成功:**
```json
{
"status": "ok",
"mode": "audio",
"start": "0/1",
"duration": "48000/48000",
"sample_rate": 48000,
"channels": 2,
"sample_count": 48000,
"shm_name": "/olive_audio_abc123",
"data_offset": 256,
"data_size": 384000,
"render_time_ms": 15
}
```
### 4.4 字段说明
| 字段 | 类型 | 出现条件 | 说明 |
|---|---|---|---|
| `status` | string | 总是 | `"ok"`, `"error"`, `"cancelled"` |
| `mode` | string | 总是 | `"frame"`, `"batch"`, `"audio"` |
| `time` / `times` | string | frame/batch | 渲染时间点 |
| `width` | int | frame/batch | 帧宽 |
| `height` | int | frame/batch | 帧高 |
| `format` | string | frame/batch | 像素格式名称 |
| `pixel_format_id` | int | frame/batch | 像素格式枚举值 |
| `channel_count` | int | frame/batch | 通道数 |
| `shm_name` | string | 总是 | 共享内存名称 |
| `data_offset` | int | 总是 | 实际数据在共享内存中的偏移(跳过 ShmHeader) |
| `data_size` | int | 总是 | 实际数据大小(字节) |
| `linesize` | int | frame/batch | 每行字节数(含 padding |
| `frame_count` | int | batch | 批处理帧数 |
| `frames` | array | batch | 每帧的元数据 |
| `sample_rate` | int | audio | 采样率 |
| `channels` | int | audio | 通道数 |
| `sample_count` | int | audio | 采样数 |
| `error_code` | string | error | 错误分类码 |
| `message` | string | error | 人类可读错误信息 |
| `render_time_ms` | int | 总是 | 纯渲染耗时(不含进程启动) |
---
## 5. 共享内存二进制布局
### 5.1 整体结构
```
┌─────────────────────────────────────────────────────────────────┐
│ 共享内存区域 │
├────────────────────────┬────────────────────────────────────────┤
│ ShmHeader (256 B) │ Payload Data │
│ │ │
│ magic │ 帧像素数据 / 音频采样数据 │
│ version │ │
│ data_offset │ 大小 = data_size │
│ data_size │ │
│ width │ │
│ height │ │
│ format │ │
│ linesize │ │
│ checksum │ │
│ reserved[...] │ │
└────────────────────────┴────────────────────────────────────────┘
```
### 5.2 ShmHeader 定义(C 结构)
```c
#include <stdint.h>
#define OLIVE_SHM_MAGIC 0x4F4C4956 // 'OLIV' 大端序
#define OLIVE_SHM_VERSION 1
#define OLIVE_SHM_HEADER_SIZE 256
typedef struct {
uint32_t magic; // OLIVE_SHM_MAGIC
uint32_t version; // OLIVE_SHM_VERSION
uint32_t data_offset; // Payload 数据起始偏移(>= 256
uint64_t data_size; // Payload 实际数据大小(字节)
uint32_t width; // 帧宽 或 采样数
uint32_t height; // 帧高 或 0(音频)
uint32_t depth; // 3D 纹理深度(通常 1
uint32_t channel_count; // 通道数
uint32_t pixel_format; // OlivePixelFormat 枚举值
uint32_t linesize; // 每行字节数(视频)或 0(音频)
uint64_t checksum; // CRC64 校验和(可选,0 表示未校验)
uint8_t reserved[256 - 48]; // 填充至 256 字节,未来扩展用
} OliveShmHeader;
// 辅助:计算 CRC64
uint64_t olive_shm_checksum(const void* data, size_t size);
```
### 5.3 校验和(Checksum
- 默认启用 CRC64 校验。
- `checksum` 字段覆盖 **Payload Data 区域**(从 `data_offset` 开始的 `data_size` 字节)。
- 若主进程设置 `checksum = 0`,表示跳过校验(用于调试或性能敏感场景)。
### 5.4 多帧批处理布局
批处理模式下,所有帧连续存储在同一块共享内存中:
```
Offset 0: ShmHeader (256 B)
Offset 256: Frame 0 data (frame_0_size B, 按 linesize 对齐)
Offset 256+N0: Frame 1 data (frame_1_size B)
Offset 256+N0+N1: Frame 2 data
...
```
每帧的 `data_offset` 在 stdout JSON 中单独指定。
---
## 6. 标准错误(stderr
### 6.1 日志级别
`--verbose` 启用时,stderr 输出结构化日志:
```
[2024-05-21T10:30:15.123Z] [INFO] 初始化 OpenGL 上下文
[2024-05-21T10:30:15.245Z] [INFO] OpenGL 版本: 4.6.0 NVIDIA 535.104
[2024-05-21T10:30:15.310Z] [INFO] 加载节点图: 42 个节点
[2024-05-21T10:30:15.412Z] [INFO] 开始渲染帧 @ 1001/30000
[2024-05-21T10:30:15.454Z] [INFO] 渲染完成, 耗时 42ms
[2024-05-21T10:30:15.455Z] [INFO] 写入共享内存: /olive_frame_abc123, 33177600 bytes
```
### 6.2 错误日志
错误同时输出到 stderr 和 stdout JSON
```
[2024-05-21T10:30:15.456Z] [ERROR] Decoder 初始化失败: codec not found for 'hevc'
```
---
## 7. 退出码
| 退出码 | 名称 | 含义 | 主进程应对 |
|---|---|---|---|
| 0 | `EXIT_OK` | 渲染成功 | 正常处理结果 |
| 1 | `EXIT_GENERIC_ERROR` | 通用错误 | 记录错误,丢弃该帧 |
| 2 | `EXIT_INVALID_ARGS` | 命令行参数无效 | 检查主进程参数组装逻辑 |
| 3 | `EXIT_INIT_FAILED` | 初始化失败(OpenGL/OCIO | 尝试 `dummy` 后端或提示用户 |
| 4 | `EXIT_GRAPH_LOAD_FAILED` | 节点图加载/解析失败 | 检查 XML 序列化逻辑 |
| 5 | `EXIT_RENDER_FAILED` | 渲染过程中出错 | 记录具体错误,丢弃该帧 |
| 6 | `EXIT_OUTPUT_FAILED` | 输出写入失败(SHM 不足) | 清理 SHM,重试或报错 |
| 130 | `EXIT_SIGINT` | 收到 SIGINTCtrl+C / kill -2 | 视为取消,正常丢弃 |
| 137 | `EXIT_SIGKILL` | 收到 SIGKILLkill -9 | 视为取消,正常丢弃 |
| 139 | `EXIT_SEGFAULT` | 段错误(未捕获信号) | 视为崩溃,记录日志 |
| 其他 | — | 未知错误 | 记录日志,丢弃该帧 |
---
## 8. 主进程与子进程交互时序
### 8.1 单帧完整时序
```
时间轴 ──────────────────────────────────────────────────────────────>
主进程: [准备参数] [创建SHM] [QProcess::start()] [等待] [读JSON] [读SHM] [unlink SHM]
│ │ │ │ │ │ │
子进程: [启动] [解析参数] [加载XML] [初始化GL] [渲染] [写SHM] [写stdout] [exit]
│ │ │ │ │ │ │
└────────┴────────┴───────────┴────────┴────────┴──────────┘
进程生命周期
```
### 8.2 异常情况时序
**子进程崩溃(segfault**
```
主进程: [start] ── [wait] ── [finished信号] ── [exitStatus == CrashExit] ── [忽略该帧]
子进程: [启动] ── [崩溃] ──────────────────── [操作系统回收资源]
```
**主进程取消(kill**
```
主进程: [start] ── [用户操作/超时] ── [QProcess::kill()] ── [finished信号] ── [unlink SHM]
子进程: [启动] ── [渲染中] ───────── [SIGKILL] ──────────── [立即终止]
```
---
## 9. 共享内存生命周期管理
### 9.1 创建
```cpp
// 主进程创建
QString shm_name = "/olive_" + QUuid::createUuid().toString(QUuid::WithoutBraces);
size_t shm_size = CalculateFrameSize(params) + OLIVE_SHM_HEADER_SIZE;
void* shm_ptr = olive_shm_create(shm_name.toUtf8().constData(), shm_size);
```
### 9.2 命名规范
- POSIX: `/olive_<uuid>`,必须以 `/` 开头,长度 < 255。
- Windows: `Local\\olive_<uuid>``Global\\olive_<uuid>`
- 临时文件: `/tmp/olive_<pid>_<uuid>.raw`(内存映射临时文件回退方案)。
### 9.3 清理策略
**正常路径**
1. 子进程成功渲染,写入数据,退出。
2. 主进程读取数据。
3. 主进程 `olive_shm_close(ptr, size)` + `olive_shm_unlink(name)`
**异常路径(子进程崩溃)**
1. 主进程检测到 `QProcess::CrashExit`
2. 主进程立即 `olive_shm_unlink(name)`(即使数据未读取)。
**双重保险**
- 主进程在创建 SHM 时启动一个 `QTimer`5 秒后触发)。
- 若 5 秒后 SHM 仍未被清理(异常路径未执行到 unlink),定时器自动 `olive_shm_unlink(name)`
- 防止子进程崩溃后主进程也崩溃导致的 SHM 泄漏。
---
## 10. 平台差异
### 10.1 Linux
- 共享内存:`/dev/shm/` 下的 tmpfs 文件。
- 最大名称长度:255 字节(含 null)。
- 权限:`shm_open` 使用 `0666`,确保子进程可以打开。
- 系统限制:`/proc/sys/kernel/shmmax` 通常足够大(> 1GB)。
### 10.2 macOS
- 共享内存:`shm_open` 创建的 POSIX 共享内存对象。
- 注意:macOS 的 `shm_open` 名称长度限制为 31 字符(包括开头的 `/`)!
- **解决方案**:使用 **内存映射临时文件** 替代 POSIX shm。
```cpp
// macOS 专用:使用临时文件替代 shm_open
int fd = mkstemp("/tmp/olive_XXXXXX.raw");
ftruncate(fd, size);
void* ptr = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// 将文件路径传递给子进程
```
### 10.3 Windows
- 共享内存:`CreateFileMapping` + `MapViewOfFile`
- 名称:`Local\\olive_<uuid>`(用户会话内可见)。
- 注意:句柄管理。子进程 `MapViewOfFile` 后需要保留 `HANDLE` 以便后续 `UnmapViewOfFile``CloseHandle`
- 简化方案:子进程只负责写入,不关闭映射。进程退出后操作系统自动回收。
---
## 11. 性能调优建议
### 11.1 减少进程启动开销
| 技术 | 效果 | 复杂度 |
|---|---|---|
| 静态链接 `olive-renderer` | 避免动态库加载开销 | 低 |
| 预加载 Qt 插件 | 减少 `QCoreApplication` 初始化时间 | 低 |
| 批处理模式 | 摊销 OpenGL 上下文创建开销 | 中 |
| 使用 `EGL` 替代 `GLX`/`WGL` | EGL 上下文创建更快 | 中 |
### 11.2 减少序列化开销
| 技术 | 效果 | 复杂度 |
|---|---|---|
| 节点图 XML 缓存 | 相同图只序列化一次 | 低 |
| 增量参数更新 | 仅发送变更的参数 | 中 |
| 二进制序列化格式 | 替代 XML,解析更快 | 高 |
### 11.3 减少共享内存开销
| 技术 | 效果 | 复杂度 |
|---|---|---|
| 共享内存池 | 预分配 N 块循环使用 | 中 |
| 内存映射临时文件 | 避免 `shm_open` 系统调用 | 低 |
| 零拷贝(GPU 纹理共享) | 跨进程直接共享 GPU 纹理 | 高(平台相关) |
---
## 12. 调试工具
### 12.1 手动运行子进程
```bash
# 直接运行 olive-renderer,独立于主进程
./olive-renderer \
--mode=frame \
--node-graph=/tmp/debug_graph.xml \
--time=0/1 \
--video-params='{"width":100,"height":100,"format":"rgba32f"}' \
--output-shm=/olive_debug \
--output-shm-size=160000 \
--verbose
# 查看 stdout 输出
# 查看 stderr 日志
# 用另一个程序读取 /olive_debug 验证像素数据
```
### 12.2 环境变量
| 变量 | 作用 |
|---|---|
| `OLIVE_RENDERER_BACKEND=dummy` | 强制使用 dummy 后端(跳过 OpenGL |
| `OLIVE_RENDERER_TIMEOUT=60000` | 子进程超时时间(毫秒) |
| `OLIVE_RENDERER_KEEP_SHM=1` | 子进程退出后不删除共享内存(调试) |
| `OLIVE_RENDERER_LOG_FILE=/path/to.log` | 将日志写入文件 |
### 12.3 重放渲染
将主进程发送给子进程的所有输入(命令行参数 + XML)保存到日志目录,可以精确重放某一帧的渲染:
```bash
# 主进程日志目录:~/.local/share/oak/renderer_logs/
# 每个渲染任务保存为:
# frame_12345.params (命令行参数)
# frame_12345.xml (节点图)
# 重放
./olive-renderer $(cat ~/.local/share/oak/renderer_logs/frame_12345.params)
```
-114
View File
@@ -1,114 +0,0 @@
# Olive/Oak 模块化与多进程渲染:显式加载 + 纯 C 接口方案
> **状态**:详细实施计划
> **范围**:仅制定方案,不涉及代码变更。
> **核心目标**
> 1. 所有业务模块编译为动态库,主进程**显式加载**`dlopen`/`LoadLibrary`),通过**纯 C 接口**交互。
> 2. 渲染引擎拆分为独立可执行文件 `olive-renderer`,采用**"用完即弃"**的进程模型:每帧(或每几帧)启动一个新进程,渲染完成后立即退出,最大限度避免锁和状态同步问题。
---
## 目录索引
| 文件 | 内容 |
|---|---|
| `README.md` | 本文档:总体架构、设计哲学、目录索引 |
| `09-c-api-design.md` | **先读此文件**:C API 设计规范、显式加载器、内存管理、错误处理、类型映射总纲。所有其他库的 C API 都遵循此规范。 |
| `01-olivecore.md` | `libolivecore.so`:基础数据类型库(Rational, Color, TimeRange, PixelFormat, SampleBuffer 等) |
| `02-olivecodec.md` | `libolivecodec.so`:编解码库(Decoder, Encoder, Frame, Stream |
| `03-oliveplugin.md` | `liboliveplugin.so`OFX 插件宿主支持 |
| `04-oliveaudio.md` | `liboliveaudio.so`:音频播放与处理 |
| `05-olivenode.md` | `libolivenode.so`:节点图系统(Node, NodeGraph, Project, Param, Keyframe, Timeline, Undo |
| `06-oliverender.md` | `liboliverender.so`:渲染引擎抽象(RenderContext, RenderJob, RenderResult |
| `07-oliveui.md` | `liboliveui.so`UI 层(Widget, Panel, Window, Dialog |
| `08-olive-renderer.md` | `olive-renderer` 可执行文件:多进程渲染的"用完即弃"模型详细设计 |
| `10-implementation-roadmap.md` | 10 周小步快跑实施路线图,含每周任务、验收标准、回退策略 |
| `11-ipc-protocol.md` | 渲染子进程的 IPC 协议(命令行参数、stdin JSON、stdout NDJSON、共享内存) |
---
## 总体架构图
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ olive-editor(主进程,GUI
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ModuleLoader(显式加载管理器) │ │
│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │
│ │ │dlopen │ │dlopen │ │dlopen │ │dlopen │ │dlopen │ │ │
│ │ │olivecore │ │olivecodec│ │olivenode │ │oliverender││oliveui │ │ │
│ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ │
│ │ │ │ │ │ │ │ │
│ │ └────────────┴────────────┴────────────┘ │ │ │
│ │ 纯 C 接口交互 │ │ │
│ │ ▼ │ │
│ │ ┌──────────────┐ │ │
│ │ │ QProcess │ │ │
│ │ │ 启动/等待/回收 │ │ │
│ │ └──────┬───────┘ │ │
│ └───────────────────────────────────────────────────────────┼───────────┘ │
│ │ │
├──────────────────────────────────────────────────────────────┼───────────────┤
│ │ │
│ ┌───────────────────────────────────────────────────────────┘ │
│ │ olive-renderer(子进程,无 GUI"用完即弃"
│ │ │
│ │ 启动参数:--node-graph=/tmp/g.xml --time=1001/30000 --output-shm=/o_123 │
│ │ │
│ │ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ 1. 初始化 Qt Core │ │
│ │ │ 2. 初始化 OpenGL 离屏上下文 │ │
│ │ │ 3. 加载节点图 XML │ │
│ │ │ 4. 执行渲染(RenderProcessor + OpenGLRenderer │ │
│ │ │ 5. 将帧数据写入共享内存 │ │
│ │ │ 6. 输出 JSON 结果到 stdout │ │
│ │ │ 7. 清理并退出(return 0 │ │
│ │ └─────────────────────────────────────────────────────────────────────┘ │
│ └────────────────────────────────────────────────────────────────────────────┘
│ 所有动态库共享的基础依赖:QtCore, FFmpeg, OpenColorIO 等(由操作系统加载器解析)
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 核心设计哲学
### 1. 显式加载 + 纯 C 接口
- **主进程不链接任何业务动态库**。`olive-editor``main.cpp``core.cpp` 中没有任何对业务模块的 `#include`(除 C API 头文件外)。
- 所有跨模块边界的交互通过**纯 C 函数**完成,使用**不透明指针(Opaque Pointer**封装 C++ 对象。
- 动态库内部可以继续使用 C++、Qt、STL、虚函数、模板等任意特性,但对外仅暴露 C 接口。
**为什么用 C 接口而非 C++ 类?**
- C++ 的 ABI(虚表布局、name mangling、异常传播)在不同编译器/版本间不兼容。
- C 接口的符号名干净(无 mangling),`dlsym` 可直接查找。
- 未来如果需要,C 接口可被 Python、Rust、C# 等语言直接绑定。
### 2. "用完即弃"的渲染进程
- **每一帧(或每 N 帧)渲染任务 = 一个独立的操作系统进程**。
- 进程启动时接收完整的渲染参数和节点图,渲染完成后立即 `exit(0)`
- **不存在常驻渲染进程**,因此不需要:心跳检测、崩溃恢复、状态同步、读写锁、graph_ref 缓存、复杂的取消机制。
- 主进程如果需要取消渲染,直接 `QProcess::kill()` 即可。
**可能的问题与回退**
- 若进程启动开销(`QProcess::start()` + OpenGL 上下文初始化)导致实时预览帧率不足,可回退为**"批处理模式"**:每 3–5 帧共享一个进程,或预先启动一个进程池(但进程池内的进程仍不共享状态,每个进程只处理一个批次后自杀)。详见 `08-olive-renderer.md`
### 3. 小步快跑
- 每个模块的改造都是**独立、可回退、可并行**的。
- 不改现有 C++ 类定义,只在其上层**新增 C 封装层**(`api/<module>_api.cpp`)。
- 保留原有 OBJECT 库编译方式,通过 CMake 选项 `-DOLIVE_DYNAMIC_MODULES=ON` 切换。
- 每完成一个模块,就通过该模块的单元测试验证,再进入下一个模块。
---
## 快速开始(阅读顺序)
1. **先读 `09-c-api-design.md`**:理解 C API 的约定、显式加载器、内存管理规则。
2. **再读 `08-olive-renderer.md`**:理解最核心的架构变革——多进程渲染。
3. **然后按任意顺序阅读 `01-` 到 `07-`**:各业务模块的具体 C API 设计和实施步骤。
4. **最后读 `10-implementation-roadmap.md`**:10 周实施计划,了解如何排期和验收。
5. **参考 `11-ipc-protocol.md`**:渲染子进程的通信协议细节。
+258
View File
@@ -0,0 +1,258 @@
# 渲染独立进程化 — 实现计划
> **状态**:实施中(阶段 0、阶段 1 已完成)
> **分支**`feat/render-process-isolation`
> **范围**:把视频帧渲染拆到独立进程,主进程通过共享内存 + stdio 调度多个渲染 worker,全程无锁。
---
## 1. 背景(为什么做)
OakOlive 分叉,Qt6/C++17 视频编辑器)当前是**单进程**架构:所有渲染在主进程的后台
`QThread` 里完成(`app/render/rendermanager.cpp``video_thread_` / `audio_thread_` /
`waveform_threads_` 等),通过 `RenderManager::RenderFrame()``RenderThread` 队列 →
`RenderProcessor::Process()` 的 ticket 异步管线工作。
把渲染留在主进程有三个问题:
1. **崩溃传染** —— OFX 第三方插件(0.3 里程碑的核心目标“任意 OFX 插件加载不崩溃”)一旦崩溃,会带走整个编辑器,丢失未保存的工作。
2. **难以横向扩展** —— GPU 上下文、解码器缓存都绑在一个进程里,无法利用多核/多 GPU 并行。
3. **预渲染受限** —— 预渲染窗口(见 `TODO.md` 的 LRU 预渲染计划)受单进程资源约束。
**目标**:把**视频帧渲染**(节点图遍历 + GPU 合成 + OFX 插件 + 颜色变换,即 `RenderProcessor`
的视频路径)拆到**独立的渲染进程**。主进程作为调度器,通过**共享内存 + stdio** 与**多个**渲染
worker 通信。硬性要求**无锁**:跨进程数据交换走预分配的共享内存 slot 池 + SPSC 环形索引队列,
控制平面走 stdio 上的换行分隔消息。
### 1.1 已确认的范围决策
| 维度 | 决策 |
|---|---|
| **拆分范围** | 仅**视频帧渲染**。音频/波形/dry-run 暂留主进程。→ worker 链接 OpenGL / OCIO / OpenImageIO / OFX**不**链接 UIWidgets)。 |
| **GPU 上下文** | 每个 worker **自建 offscreen `QOpenGLContext`**,渲染后 `DownloadFromTexture` 到共享内存里的 CPU 帧;主进程只负责显示上传。 |
| **素材输入** | **主进程解码**(复用现有 `DecoderCache`),把解码后的原始帧经共享内存喂给 worker。→ worker **不**链接 FFmpeg。 |
| **图同步** | **全量序列化**整个节点图(复用 `ProjectSerializer`),架构预留增量通道。 |
| **帧回传** | **固定 slot 池 + 无锁环形队列**(按最大分辨率预分配)。 |
| **控制协议** | **纯文本 NDJSON**(每行一条 JSON),便于 `cat`/`tee` 调试、手工注入测试。大块图数据走临时文件传路径。 |
| **落地策略** | **分阶段**,每步可编译可验证,旧的进程内渲染保留为默认,用开关切换。 |
---
## 2. 现有架构锚点(复用,不重写)
| 关注点 | 文件 / 符号 |
|---|---|
| 渲染调度/线程池 | `app/render/rendermanager.{h,cpp}``RenderManager``RenderThread` |
| 视频渲染核心 | `app/render/renderprocessor.{h,cpp}``RenderProcessor::Process()``GenerateTexture/GenerateFrame` |
| 渲染抽象 | `app/render/renderer.h``app/render/opengl/openglrenderer.{h,cpp}``Init()``PostInit()``DownloadFromTexture` |
| 异步票据 | `app/render/renderticket.{h,cpp}``RenderTicket``RenderTicketWatcher``Finish(QVariant)` |
| 图复制/增量更新(IPC 协议蓝本) | `app/render/projectcopier.{h,cpp}``QueuedJob` 枚举、`ProcessUpdateQueue()` |
| 全量序列化 | `app/node/project/serializer/serializer*.{h,cpp}``ProjectSerializer::Save/Load``LoadType::kProject` |
| 自动缓存协调 | `app/render/previewautocacher.{h,cpp}` — 票据的实际消费者 |
| 帧内存(单段连续 buffer) | `app/codec/frame.{h,cpp}` + `app/render/framemanager.h``data_`/`linesize_`/`allocated_size()` |
| 帧消费/显示 | `app/widget/viewer/viewer.cpp``SetDisplayImage()``ticket->Get()` |
| 进程入口 | `app/main.cpp``QSurfaceFormat` 设置(OpenGL 3.2 core)、`AA_ShareOpenGLContexts` |
| 构建 | 根 `CMakeLists.txt``app/CMakeLists.txt``add_executable(olive-editor ...)` + `libolive-editor` OBJECT 库 |
**关键观察**
- `RenderProcessor::Process()` 已是无状态静态入口,参数全在 `ticket->property(...)` 里。这是进程边界的天然切割点。
- `Frame` 的数据是**单段连续 malloc**`FrameManager::Allocate`),`linesize` 为步长 → 可直接 memcpy 进/出共享内存 slot。
- `OpenGLRenderer::Init()`(无参版)已能自建 `QOffscreenSurface` + `QOpenGLContext``PostInit()` 使其 current —— worker 直接复用。
- 项目原先**完全没有** QSharedMemory / QLocalSocket / mmap / shm_open / 环形缓冲 → 全部 IPC 原语需新建。
- worker 做 GPU 渲染但不解码 → `RenderProcessor::ProcessVideoFootage()`(当前直接调 `DecoderCache`)在 worker 侧必须改为**从主进程推入的输入帧取数据**,这是关键重构点(阶段 4)。
---
## 3. 目标架构
```
┌─────────────────── 主进程 (olive-editor) ───────────────────┐
│ Viewer / PreviewAutoCacher │
│ │ GetSingleFrame() │
│ ▼ │
│ RenderManager (调度器) │
│ ├─ DecoderCache ← 解码原始素材帧 │
│ ├─ RenderWorkerPool ← 新增 │
│ │ ├─ WorkerProcess #0 (QProcess + stdio + SHM) │
│ │ ├─ WorkerProcess #1 │
│ │ └─ ... │
│ └─ ProjectSerializer ← 全量图快照 │
└─────────────────────────────────────────────────────────────┘
stdio (控制平面: NDJSON, 每行一条 JSON 消息)
SHM (数据平面: 输入素材帧 slot 池 + 输出帧 slot 池, 无锁环形索引)
┌──────────────── 渲染进程 (olive-render-worker) ×N ───────────┐
│ workermain: 读 stdin NDJSON 控制循环 │
│ ├─ 反序列化节点图 (ProjectSerializer::Load) │
│ ├─ offscreen QOpenGLContext + OpenGLRenderer │
│ ├─ RenderProcessor (视频路径; ProcessVideoFootage 改为 │
│ │ 从输入 SHM slot 取帧, 不再直接解码) │
│ └─ DownloadFromTexture → 写输出 SHM slot → 发 frame_ready │
└─────────────────────────────────────────────────────────────┘
```
### 3.1 无锁 IPC 设计
**控制平面(stdio**worker 的 stdin/stdout**纯文本 NDJSON**——每条消息一行
compact `QJsonObject``\n` 结尾。仅承载低频控制流量(握手、提交任务、取消、关闭)。
纯文本便于 `cat`/`tee` 抓管道调试、手工注入测试;单读单写天然无锁。诊断信息走 stderr,
绝不污染 stdout 控制通道。**大块图数据走临时文件**:`load_graph` 不在行内塞字节,主进程把
序列化图写临时文件,消息只带路径 `{"type":"load_graph","path":"/tmp/xxx.ove"}`
**数据平面(共享内存)**:每个 worker 一段共享内存,封装在 `SharedMemoryRegion`
POSIX `shm_open`+`mmap` / Windows `CreateFileMapping`+`MapViewOfFile`)。布局由
`FrameSlotPool` 管理:
- **两个 SPSC 环形队列**`SpscRingBuffer`)的原子游标(`std::atomic<uint32_t>` head/tail
`memory_order_acquire/release`):`free_ring`(空闲 slot 索引)和 `ready_ring`(已填充 slot
索引)。每个环单生产者单消费者 → 无需互斥锁。
- **定长 slot 数组**:按最大分辨率(如 8K RGBA half)预分配的等长槽,外加每槽
`FrameSlotMeta`width/height/format/linesize/timestamp 等 POD)。
- **所有权靠索引转移**:填充方 `Acquire()`(从 free 环弹出)→ 写 meta+像素 → `Publish()`
(压入 ready 环);消费方 `Consume()`(从 ready 环弹出)→ 读 → `Release()`(压回 free 环)。
环满即天然背压,无需额外锁。
一个 pool 建模单向帧流。输出方向(worker→主)放渲染结果;输入方向(主→worker)放解码素材。
---
## 4. 分阶段实现计划
> 每个阶段都能独立编译、独立验证。前期阶段不改变现有行为(进程内渲染仍是默认),
> 用开关切到多进程路径,最后再切默认。
### ✅ 阶段 0:IPC 基础设施(已完成)
新增 `app/render/ipc/` 模块:
- `spscringbuffer.h` —— header-only`std::atomic` 游标的单生产者单消费者环形索引队列,POD,可直接放共享内存。
- `sharedmemoryregion.{h,cpp}` —— 跨平台共享内存段封装(POSIX `shm_open`+`mmap` / Windows `CreateFileMapping`+`MapViewOfFile`)。直接用原生 API 而非 `QSharedMemory`(后者带隐式信号量与引用计数,不适合大帧)。
- `frameslotpool.{h,cpp}` —— 在共享内存段上布局两个环 + 定长 slot 池;提供 `Acquire/Publish/Consume/Release``FrameSlotMeta`
- `ipcmessage.{h,cpp}` —— NDJSON 控制消息编解码(`WriteMessage`/`ReadMessage` + 各类型的 `ToJson/FromJson`)。
**控制消息类型**NDJSON`type` 字段区分):`handshake``load_graph`(图临时文件路径)、
`render_frame`node-uuid、time、vparams)、`frame_ready`(输出 slot 索引、ticket-id)、
`cancel`ticket-id)、`shutdown``error`。预留 `graph_update` 增量类型(阶段 6 实现)。
**测试**`tests/gtest/render_ipc_test.cpp`Google Test):
- 环形队列:基础语义 + 回绕 + **并发 200 万值** FIFO 无丢失无重复。
- slot 池:单线程握手 + 耗尽/回填 + **并发 20 万帧**数据完整性。
- NDJSON:类型往返 + 逐字节半包 + 畸形行跳过 + 错误类型拒绝。
> 注意:`SpscRingBuffer` 内部数组访问器命名为 `slot_array()` 而非 `slots()`,以规避 Qt 的 `slots` 宏。
### ✅ 阶段 1:worker 可执行目标(已完成)
- `app/CMakeLists.txt` 新增 `add_executable(olive-render-worker ...)`,复用 `libolive-editor` OBJECT 库 + `olive-version-obj`,与 `olive-gtest` 同款链接方式。
- 新增 `app/render/worker/workermain.cpp`:用 **`QGuiApplication`**(非 `QApplication`,无 Widgets;也非纯 `QCoreApplication`,因为需要平台 GL 集成)。
- 安装与主进程一致的 `QSurfaceFormat`OpenGL 3.2 core24 位深度),设置 `AA_UseDesktopOpenGL` / `AA_ShareOpenGLContexts`
- 当前行为:`OpenGLRenderer::Init()` + `PostInit()` 建 offscreen GL 上下文 → 校验 `context()->isValid()` → 在 stdout 打一行 NDJSON 握手(含实际 GL 版本)→ 干净退出。
- **链接说明**:当前用全量 `OLIVE_LIBRARIES`(含 Widgets/FFmpeg),裁剪 UI-only 依赖留到后续阶段。
**验证结果**worker 在默认平台与 `-platform offscreen` 下均成功输出
`{"gl_major":3,"gl_minor":2,...,"type":"handshake"}`stdout 仅一行合法 JSON,退出码 0。
### 阶段 2:worker 主循环 + 单帧渲染回路(基础回路已接入)
-`workermain.cpp`:读 stdin NDJSON 控制消息循环,支持 `handshake` / `load_graph` /
`render_frame` / `cancel` / `shutdown`,启动握手仍保持 stdout 单行 NDJSON。
-`load_graph``ProjectSerializer::Load(LoadType::kProject)` 反序列化出 `Project` + 节点图;
`ProjectSerializer::LoadData` 现在暴露旧 ptr token → 新 `Node*` 映射,worker 用它解析
`render_frame.node`。旧版 serializer 已有的 node UUID 映射也保留兼容。
-`render_frame` → 构造本地 `RenderTicket`(参数从消息填 property,复刻
`RenderManager::RenderFrame` 的关键 `setProperty`)→
`RenderProcessor::Process(ticket, renderer, decoder_cache=nullptr, shader_cache)`
- ✅ 先**不**接输入素材:渲染结果为 `FramePtr` 后写入输出 `FrameSlotPool` slot
`FrameSlotMeta`,发布 slot 并回 `frame_ready`
- ✅ 临时测试驱动启动 1 个 worker,加载最小 SolidGenerator 项目,主进程从输出 slot
读回 64x64 F32 RGBA 帧并校验元数据与像素非零。待固化为自动化测试。
**验证结果**
- `cmake --build build --target olive-render-worker olive-gtest -j2` 通过。
- `QT_QPA_PLATFORM=offscreen build/tests/gtest/olive-gtest --gtest_filter='SpscRingBuffer*:*FrameSlotPool*:*IpcMessage*:*ProjectSerializer*' --gtest_brief=1`
通过,11 个测试全部通过。
- 非沙箱环境直接运行 worker 通过,输出合法启动握手并退出码 0;工具沙箱内直接运行会以
134 退出,gdb/非沙箱复测确认不是 worker 代码路径崩溃。
- 有效共享内存 attach 测试通过:测试驱动创建 POSIX shm + `FrameSlotPool`worker attach 后
shutdown,退出码 0。
- 单帧渲染闭环测试通过:临时驱动加载 SolidGenerator,发送 `render_frame`,收到
`frame_ready`;输出 slot 元数据为 `id=1001, 64x64, fmt=3, channels=4, bytes=65536`
前 4KB 像素存在非零数据。
### 阶段 3:主进程 WorkerPool + 调度器接线
- 新增 `app/render/renderworkerpool.{h,cpp}`
- `QProcess` 启动 N 个 `olive-render-worker`,建立各自 SHM 段 + stdio 管道。
- 维护 worker 忙闲状态,按最少负载派发。
- `SubmitFrame(RenderTicketPtr)`:编码 `render_frame` 派发 → worker 回 `frame_ready` 后从输出 slot 拷出 `FramePtr``ticket->Finish(...)`。**对上层透明**`RenderTicketWatcher`/`Viewer` 无感知。
- `RenderManager` 增加 `kMultiProcess` backend 分支(与 `kOpenGL` 并存),多进程模式下 `RenderFrame()``RenderWorkerPool`
- config 开关控制启用,默认仍走进程内 `kOpenGL`
- 验证:开关打开后 Viewer 正常播放纯生成内容;关掉回退旧路径无差异。
### 阶段 4:素材输入解耦(关键重构)
- `RenderProcessor::ProcessVideoFootage()``renderprocessor.cpp:397`)当前经 `ResolveDecoderFromInput` + `DecoderCache` 解码。worker 不链接 FFmpeg,需改为从输入 slot 取已解码帧上传纹理。
- 主进程侧 `RenderWorkerPool` 派发前用 `DecoderCache` 解出所需原始帧写入输入 slot,索引随 `render_frame` 一起发。
- 先支持单素材片段,再扩展到多层/转场。
- 验证:渲染含真实素材的时间线帧,与单进程结果逐像素一致。
### 阶段 5:多 worker、取消、健壮性
- WorkerPool 扩到多 worker 并行预渲染窗口(对接 `PreviewAutoCacher` 范围缓存)。
- `cancel`:取消票据时通知 worker 丢弃在途任务(复用 `CancelableObject`/`RenderTicket::IsCancelled`)。
- worker 崩溃检测(`QProcess::finished` 异常码)→ 自动重启 + 重发 `load_graph` + 重派未完成票据。这是 OFX 崩溃隔离收益的兑现点。
- 背压:slot 池/环满时调度器暂缓派发(环满即天然背压)。
### 阶段 6:图增量同步(可选优化)
-`ProjectCopier``QueuedJob`kNodeAdded/kEdgeAdded/kValueChanged…)编码成 `graph_update` 消息,worker 侧等价 `ProcessUpdateQueue`,省去每次全量序列化。
- 阶段 0 已预留消息类型,此处填实现。
---
## 5. 文件清单
**新增**
| 文件 | 阶段 | 状态 |
|---|---|---|
| `app/render/ipc/spscringbuffer.h` | 0 | ✅ |
| `app/render/ipc/sharedmemoryregion.{h,cpp}` | 0 | ✅ |
| `app/render/ipc/frameslotpool.{h,cpp}` | 0 | ✅ |
| `app/render/ipc/ipcmessage.{h,cpp}` | 0 | ✅ |
| `app/render/ipc/CMakeLists.txt` | 0 | ✅ |
| `tests/gtest/render_ipc_test.cpp` | 0 | ✅ |
| `app/render/worker/workermain.cpp` | 1/2 | ✅ 基础主循环 |
| `app/render/renderworkerpool.{h,cpp}` | 3 | 待办 |
**修改**
| 文件 | 阶段 | 状态 |
|---|---|---|
| `app/render/CMakeLists.txt`(加 `add_subdirectory(ipc)` | 0 | ✅ |
| `tests/gtest/CMakeLists.txt`(注册 ipc 测试) | 0 | ✅ |
| `app/CMakeLists.txt`(新增 `olive-render-worker` target | 1 | ✅ |
| `app/node/project/serializer/serializer*.{h,cpp}`(暴露加载映射供 worker 查节点) | 2 | ✅ |
| `app/render/rendermanager.{h,cpp}``kMultiProcess` 分支 + WorkerPool 接线) | 3 | 待办 |
| `app/render/renderprocessor.cpp``ProcessVideoFootage` 改取输入 slot | 4 | 待办 |
| `app/config/config.h`(多进程开关) | 3 | 待办 |
---
## 6. 验证方式(端到端)
1. **IPC 单元测试**:多线程压测 SPSC 环形队列 + slot 池,确认无锁正确性(无丢失/重复/数据竞争,可配 TSan)。— 阶段 0 已覆盖。
2. **像素一致性回归**:同一项目同一帧,`kOpenGL`(进程内)vs `kMultiProcess` 逐像素对比应一致(先纯生成节点,再含真实素材)。
3. **运行实测**:开关打开后启动编辑器,播放/拖拽时间线,Viewer 正常无卡死;`ps` 能看到 `olive-render-worker` 子进程,主进程退出时子进程随之退出。
4. **崩溃隔离**:人为让 worker 段错误(或加载会崩的 OFX 插件),确认主进程存活、WorkerPool 自动重启并恢复渲染。
5. **性能**:多 worker 预渲染窗口吞吐 vs 单进程基线对比。
---
## 7. 开放问题(实现时定)
- SHM slot 尺寸/数量的默认值(按硬件分档,参考 `TODO.md` 同款问题)。
- worker 数默认值(CPU/GPU 数推导)。
- OFX 插件在多 worker 下的句柄/许可证并发是否有限制。
- worker 链接集裁剪时机:何时安全移除 Widgets/FFmpeg 依赖(依赖阶段 4 素材解耦完成)。
+1
View File
@@ -12,6 +12,7 @@ add_executable(olive-gtest
render_audioparams_branch_test.cpp
render_sampleformat_test.cpp
render_pixelformat_test.cpp
render_ipc_test.cpp
project_serializer_test.cpp
timeline_marker_test.cpp
undo_stack_test.cpp
+4
View File
@@ -50,6 +50,10 @@ TEST(ProjectSerializer, SaveLoadProjectRoundTrip)
olive::ProjectSerializer::kProject);
EXPECT_EQ(result.code(), olive::ProjectSerializer::kSuccess);
EXPECT_FALSE(loaded_project.nodes().isEmpty());
ASSERT_TRUE(result.GetLoadData().node_ptrs.contains(
reinterpret_cast<quintptr>(node)));
EXPECT_TRUE(loaded_project.nodes().contains(
result.GetLoadData().node_ptrs.value(reinterpret_cast<quintptr>(node))));
olive::ProjectSerializer::Destroy();
if (created_disk_manager) {
+381
View File
@@ -0,0 +1,381 @@
/*
* Oak Video Editor - Render IPC Primitive Tests
* Copyright (C) 2026 Oak Team
*
* Unit tests for the lock-free cross-process render IPC primitives:
* - SpscRingBuffer (single-producer/single-consumer lock-free index queue)
* - FrameSlotPool (shared-memory frame slot hand-off via two SPSC rings)
* - NDJSON control message encode/decode and framing
*
* The threaded tests stress the lock-free invariants (no loss, no duplication, FIFO order) and are
* intended to be run under ThreadSanitizer in CI as well.
*/
#include <gtest/gtest.h>
#include <atomic>
#include <cstring>
#include <thread>
#include <vector>
#include <QBuffer>
#include <QJsonDocument>
#include <QJsonObject>
#include "render/ipc/frameslotpool.h"
#include "render/ipc/ipcmessage.h"
#include "render/ipc/spscringbuffer.h"
using namespace olive::ipc;
// ============================================================================
// SpscRingBuffer
// ============================================================================
TEST(SpscRingBuffer, BasicPushPopAndCapacity)
{
std::vector<uint8_t> mem(SpscRingBuffer::BytesNeeded(4));
SpscRingBuffer *ring = SpscRingBuffer::Create(mem.data(), 4);
EXPECT_TRUE(ring->IsEmptyApprox());
uint32_t v = 0;
EXPECT_FALSE(ring->Pop(&v)); // empty
// Capacity 4 holds at most 3 entries (one slot reserved to disambiguate full/empty).
EXPECT_TRUE(ring->Push(10));
EXPECT_TRUE(ring->Push(20));
EXPECT_TRUE(ring->Push(30));
EXPECT_FALSE(ring->Push(40)); // full
EXPECT_TRUE(ring->Pop(&v));
EXPECT_EQ(v, 10u);
EXPECT_TRUE(ring->Pop(&v));
EXPECT_EQ(v, 20u);
EXPECT_TRUE(ring->Pop(&v));
EXPECT_EQ(v, 30u);
EXPECT_FALSE(ring->Pop(&v)); // empty again
}
TEST(SpscRingBuffer, WrapAround)
{
std::vector<uint8_t> mem(SpscRingBuffer::BytesNeeded(4));
SpscRingBuffer *ring = SpscRingBuffer::Create(mem.data(), 4);
// Repeatedly pushing then popping single values forces the cursors past the backing array end.
for (uint32_t i = 0; i < 100; i++) {
ASSERT_TRUE(ring->Push(i));
uint32_t got = 0;
ASSERT_TRUE(ring->Pop(&got));
EXPECT_EQ(got, i);
}
EXPECT_TRUE(ring->IsEmptyApprox());
}
TEST(SpscRingBuffer, ConcurrentProducerConsumer)
{
constexpr uint32_t kCapacity = 1024;
constexpr uint32_t kCount = 2'000'000; // values 0..kCount-1 streamed through the ring
std::vector<uint8_t> mem(SpscRingBuffer::BytesNeeded(kCapacity));
SpscRingBuffer *ring = SpscRingBuffer::Create(mem.data(), kCapacity);
std::atomic<bool> order_ok{true};
std::thread producer([&] {
for (uint32_t i = 0; i < kCount; i++) {
while (!ring->Push(i)) {
std::this_thread::yield(); // buffer full, spin until consumer drains
}
}
});
std::thread consumer([&] {
// Every value must arrive exactly once and strictly in order (FIFO).
uint32_t expected = 0;
while (expected < kCount) {
uint32_t got = 0;
if (ring->Pop(&got)) {
if (got != expected) {
order_ok.store(false);
return;
}
expected++;
} else {
std::this_thread::yield();
}
}
});
producer.join();
consumer.join();
EXPECT_TRUE(order_ok.load());
EXPECT_TRUE(ring->IsEmptyApprox());
}
// ============================================================================
// FrameSlotPool
// ============================================================================
TEST(FrameSlotPool, SingleThreadedHandoff)
{
constexpr uint32_t kSlots = 3;
constexpr size_t kSlotBytes = 256;
std::vector<uint8_t> mem(FrameSlotPool::BytesNeeded(kSlots, kSlotBytes));
FrameSlotPool filler = FrameSlotPool::Create(mem.data(), kSlots, kSlotBytes);
FrameSlotPool drainer = FrameSlotPool::Attach(mem.data());
ASSERT_TRUE(filler.IsValid());
ASSERT_TRUE(drainer.IsValid());
EXPECT_EQ(drainer.slot_count(), kSlots);
EXPECT_EQ(drainer.slot_data_bytes(), kSlotBytes);
// Fill one slot with a recognizable pattern + metadata, publish, then drain and verify.
uint32_t idx = 0;
ASSERT_TRUE(filler.Acquire(&idx));
auto *data = static_cast<uint8_t *>(filler.SlotData(idx));
for (size_t i = 0; i < kSlotBytes; i++) {
data[i] = uint8_t(i & 0xFF);
}
FrameSlotMeta *meta = filler.Meta(idx);
meta->id = 4242;
meta->width = 16;
meta->height = 8;
meta->data_size = int32_t(kSlotBytes);
ASSERT_TRUE(filler.Publish(idx));
uint32_t got_idx = 0;
ASSERT_TRUE(drainer.Consume(&got_idx));
EXPECT_EQ(got_idx, idx);
const FrameSlotMeta *got_meta = drainer.Meta(got_idx);
EXPECT_EQ(got_meta->id, 4242);
EXPECT_EQ(got_meta->width, 16);
const auto *got_data = static_cast<const uint8_t *>(drainer.SlotData(got_idx));
for (size_t i = 0; i < kSlotBytes; i++) {
ASSERT_EQ(got_data[i], uint8_t(i & 0xFF));
}
EXPECT_TRUE(drainer.Release(got_idx));
}
TEST(FrameSlotPool, ExhaustionAndRefill)
{
constexpr uint32_t kSlots = 3;
constexpr size_t kSlotBytes = 64;
std::vector<uint8_t> mem(FrameSlotPool::BytesNeeded(kSlots, kSlotBytes));
FrameSlotPool pool = FrameSlotPool::Create(mem.data(), kSlots, kSlotBytes);
// Acquire every slot, then confirm the pool reports empty.
std::vector<uint32_t> held;
for (uint32_t i = 0; i < kSlots; i++) {
uint32_t a = 0;
ASSERT_TRUE(pool.Acquire(&a));
held.push_back(a);
}
uint32_t overflow = 0;
EXPECT_FALSE(pool.Acquire(&overflow)); // pool exhausted
// Publishing then consuming + releasing returns the slots to the free pool.
for (uint32_t idx : held) {
ASSERT_TRUE(pool.Publish(idx));
}
for (uint32_t i = 0; i < kSlots; i++) {
uint32_t c = 0;
ASSERT_TRUE(pool.Consume(&c));
ASSERT_TRUE(pool.Release(c));
}
uint32_t again = 0;
EXPECT_TRUE(pool.Acquire(&again)); // free again
}
TEST(FrameSlotPool, ConcurrentFillDrainIntegrity)
{
constexpr uint32_t kSlots = 8;
constexpr size_t kSlotBytes = 4096;
constexpr int64_t kFrames = 200'000;
std::vector<uint8_t> mem(FrameSlotPool::BytesNeeded(kSlots, kSlotBytes));
FrameSlotPool filler = FrameSlotPool::Create(mem.data(), kSlots, kSlotBytes);
FrameSlotPool drainer = FrameSlotPool::Attach(mem.data());
std::atomic<bool> integrity_ok{true};
// Filler: for each frame id, acquire a slot, stamp the id into meta and a pattern into the data,
// publish. Spins when no slot is free (this is the natural backpressure path).
std::thread fill_thread([&] {
for (int64_t id = 0; id < kFrames; id++) {
uint32_t idx = 0;
while (!filler.Acquire(&idx)) {
std::this_thread::yield();
}
filler.Meta(idx)->id = id;
auto *d = static_cast<uint8_t *>(filler.SlotData(idx));
const uint8_t pat = uint8_t(id & 0xFF);
memset(d, pat, kSlotBytes);
while (!filler.Publish(idx)) {
std::this_thread::yield(); // ready ring transiently full
}
}
});
// Drainer: consume in order, verify the id is monotonic and the data matches the id pattern,
// then release the slot back to the filler.
std::thread drain_thread([&] {
int64_t expected = 0;
while (expected < kFrames) {
uint32_t idx = 0;
if (!drainer.Consume(&idx)) {
std::this_thread::yield();
continue;
}
const FrameSlotMeta *m = drainer.Meta(idx);
if (m->id != expected) {
integrity_ok.store(false);
return;
}
const auto *d = static_cast<const uint8_t *>(drainer.SlotData(idx));
const uint8_t pat = uint8_t(expected & 0xFF);
if (d[0] != pat || d[kSlotBytes - 1] != pat) {
integrity_ok.store(false);
return;
}
while (!drainer.Release(idx)) {
std::this_thread::yield();
}
expected++;
}
});
fill_thread.join();
drain_thread.join();
EXPECT_TRUE(integrity_ok.load());
}
// ============================================================================
// NDJSON control messages
// ============================================================================
TEST(IpcMessage, TypedRoundTrip)
{
// Write several typed messages into a buffer, then drain and parse them back the way a pipe
// reader would.
QByteArray storage;
QBuffer dev(&storage);
ASSERT_TRUE(dev.open(QIODevice::WriteOnly));
HandshakeMsg hs;
hs.protocol_version = 1;
hs.shm_key = QStringLiteral("olive-rw-1234-0");
hs.input_slots = 4;
hs.output_slots = 6;
hs.slot_data_bytes = 256ll * 1024 * 1024;
ASSERT_TRUE(WriteMessage(&dev, hs.ToJson()));
RenderFrameMsg rf;
rf.ticket_id = 99;
rf.node_uuid = QStringLiteral("{abcd-1234}");
rf.time_num = 1001;
rf.time_den = 30000;
rf.width = 1920;
rf.height = 1080;
rf.format = 3;
rf.channel_count = 4;
rf.mode = 1;
ASSERT_TRUE(WriteMessage(&dev, rf.ToJson()));
FrameReadyMsg fr;
fr.ticket_id = 99;
fr.output_slot = 2;
ASSERT_TRUE(WriteMessage(&dev, fr.ToJson()));
dev.close();
QByteArray reader = storage;
QJsonObject obj;
bool ok = false;
ASSERT_TRUE(ReadMessage(&reader, &obj, &ok));
ASSERT_TRUE(ok);
HandshakeMsg hs2;
ASSERT_TRUE(HandshakeMsg::FromJson(obj, &hs2));
EXPECT_EQ(hs2.protocol_version, 1);
EXPECT_EQ(hs2.shm_key, hs.shm_key);
EXPECT_EQ(hs2.input_slots, 4);
EXPECT_EQ(hs2.output_slots, 6);
EXPECT_EQ(hs2.slot_data_bytes, hs.slot_data_bytes);
ASSERT_TRUE(ReadMessage(&reader, &obj, &ok));
ASSERT_TRUE(ok);
RenderFrameMsg rf2;
ASSERT_TRUE(RenderFrameMsg::FromJson(obj, &rf2));
EXPECT_EQ(rf2.ticket_id, 99);
EXPECT_EQ(rf2.node_uuid, rf.node_uuid);
EXPECT_EQ(rf2.time_num, 1001);
EXPECT_EQ(rf2.time_den, 30000);
EXPECT_EQ(rf2.width, 1920);
EXPECT_EQ(rf2.format, 3);
ASSERT_TRUE(ReadMessage(&reader, &obj, &ok));
ASSERT_TRUE(ok);
FrameReadyMsg fr2;
ASSERT_TRUE(FrameReadyMsg::FromJson(obj, &fr2));
EXPECT_EQ(fr2.ticket_id, 99);
EXPECT_EQ(fr2.output_slot, 2);
// No more complete lines remain.
EXPECT_FALSE(ReadMessage(&reader, &obj, &ok));
}
TEST(IpcMessage, PartialFrameByteByByte)
{
CancelMsg c;
c.ticket_id = 7;
const QByteArray full =
QByteArray(QJsonDocument(c.ToJson()).toJson(QJsonDocument::Compact)) + '\n';
// Feed the bytes one at a time; ReadMessage must return false until the terminating '\n'.
QByteArray reader;
QJsonObject obj;
bool ok = false;
for (int i = 0; i < full.size() - 1; i++) {
reader.append(full.at(i));
ASSERT_FALSE(ReadMessage(&reader, &obj, &ok)); // no complete line yet
}
reader.append(full.at(full.size() - 1)); // the trailing newline
ASSERT_TRUE(ReadMessage(&reader, &obj, &ok));
ASSERT_TRUE(ok);
CancelMsg c2;
ASSERT_TRUE(CancelMsg::FromJson(obj, &c2));
EXPECT_EQ(c2.ticket_id, 7);
}
TEST(IpcMessage, MalformedLineIsSkipped)
{
QByteArray reader = QByteArray("this is not json\n");
QJsonObject obj;
bool ok = true;
// A complete but malformed line is consumed and reported as not-ok, leaving the buffer drained.
EXPECT_FALSE(ReadMessage(&reader, &obj, &ok));
EXPECT_FALSE(ok);
EXPECT_TRUE(reader.isEmpty());
}
TEST(IpcMessage, WrongTypeRejected)
{
// FromJson must reject an object whose "type" does not match the target struct.
HandshakeMsg hs;
hs.protocol_version = 1;
const QJsonObject obj = hs.ToJson();
RenderFrameMsg rf;
EXPECT_FALSE(RenderFrameMsg::FromJson(obj, &rf));
}