Add render worker IPC loop
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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`,主进程通过 **stdio(stdin/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 JSON(NDJSON)**,每行一条完整 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 中传输(实时预览需要 24–30fps,stdio 带宽和 CPU 编解码开销均不可接受)。
|
||||
|
||||
#### 4.3.1 方案:POSIX / Windows 共享内存
|
||||
|
||||
**POSIX(Linux/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 ¶ms, const QString &graph_ref);
|
||||
int RequestRenderAudio(const RenderManager::RenderAudioParams ¶ms, 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 ¶ms);
|
||||
RenderTicketPtr RenderAudio(const RenderAudioParams ¶ms);
|
||||
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; // 完整的项目 XML(kOnlyNodes 模式)
|
||||
}
|
||||
|
||||
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 层无需感知底层架构变化。
|
||||
@@ -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` 的基础类型)。
|
||||
- 无项目内其他模块依赖。
|
||||
- 主要是 POD(Plain 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 改为 SHARED(1 天)
|
||||
|
||||
- [ ] 修改 `ext/core/CMakeLists.txt`:`STATIC` → `SHARED`,添加 `CXX_VISIBILITY_PRESET hidden`。
|
||||
- [ ] 为需要导出的类/函数添加导出宏。
|
||||
- [ ] 验证所有平台编译通过。
|
||||
|
||||
**验收标准**:`olivecore` 编译为 `.so`/`.dylib`/`.dll`,单元测试通过显式加载运行。
|
||||
|
||||
### Step 2: 编写 core C API(2–3 天)
|
||||
|
||||
- [ ] 编写 `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` 静态库),expat,Qt::Core
|
||||
> **包含源码**:`app/pluginSupport/`
|
||||
> **当前状态**:单体 OBJECT 库的一部分,通过 `target_link_libraries(olive-editor PUBLIC OfxHost)` 隐式链接
|
||||
> **改造难度**:⭐⭐(较简单,接口相对独立)
|
||||
|
||||
---
|
||||
|
||||
## 1. 当前状态分析
|
||||
|
||||
`app/pluginSupport/` 实现 OFX(OpenFX)标准的 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: 隔离 PluginNode(1 天)
|
||||
|
||||
- [ ] 将 `app/node/plugins/PluginNode` 移动到 `app/node/` 下(或保持原位,但确保它编译进 `libolivenode.so` 而非 `liboliveplugin.so`)。
|
||||
- [ ] 确认 `PluginNode` 对 `pluginSupport/` 的依赖方向:PluginNode 使用 pluginSupport 的类,而非相反。
|
||||
|
||||
**验收标准**:`liboliveplugin.so` 编译时不包含任何 `node/` 下的源文件。
|
||||
|
||||
### Step 1: 构建 liboliveplugin.so(1 天)
|
||||
|
||||
- [ ] 创建 `app/pluginSupport/CMakeLists.txt`(若尚无)。
|
||||
- [ ] 将 `pluginSupport/` 的源文件从主 OBJECT 库中移出,单独构建为 `oliveplugin SHARED`。
|
||||
- [ ] 确保 `OfxHost` 静态库先被构建(`third_party/openfx/HostSupport`)。
|
||||
|
||||
**验收标准**:`liboliveplugin.so` 编译成功,能通过 `dlsym` 找到 `olive_plugin_api_version`。
|
||||
|
||||
### Step 2: 最小 C API(2 天)
|
||||
|
||||
- [ ] 先实现最必需的接口:
|
||||
- `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`
|
||||
> **外部依赖**:PortAudio,Qt::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.so(1 天)
|
||||
|
||||
- [ ] 将 `app/audio/` 从主 OBJECT 库移出,单独构建为 `oliveaudio SHARED`。
|
||||
- [ ] 确保 `AudioManager` 单例的初始化顺序正确(Qt 的 `Q_GLOBAL_STATIC` 或延迟初始化)。
|
||||
|
||||
**验收标准**:`liboliveaudio.so` 编译成功。
|
||||
|
||||
### Step 2: 最小 C API(1 天)
|
||||
|
||||
- [ ] 实现播放控制:`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` 中完成。 |
|
||||
@@ -1,413 +0,0 @@
|
||||
# libolivenode.so — 节点图系统
|
||||
|
||||
> **依赖**:`libolivecore.so`, `libolivecodec.so`
|
||||
> **外部依赖**:Qt::Core(QObject, 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.so(1 天)
|
||||
|
||||
- [ ] 将 `node/`, `timeline/`, `undo/`, `config/` 的源文件聚合,构建为 `olivenode SHARED`。
|
||||
- [ ] 处理 `node/` 下的 `add_subdirectory` 嵌套,确保所有源文件被正确收集。
|
||||
|
||||
**验收标准**:`libolivenode.so` 编译成功,`nm -D libolivenode.so | grep olive_node` 能看到导出的 C 符号。
|
||||
|
||||
### Step 2: 最小 C API(2 天)
|
||||
|
||||
- [ ] 先实现项目级接口:
|
||||
- `olive_project_create/destroy/load_file/save_file`
|
||||
- `olive_node_graph_create/destroy/load_xml/save_xml`
|
||||
- [ ] 这些接口是渲染子进程最需要的:子进程需要加载 XML 节点图并渲染。
|
||||
|
||||
**验收标准**:可以用 C API 创建一个项目、保存为 XML、再加载回来,内容一致。
|
||||
|
||||
### Step 3: 节点操作 API(2 天)
|
||||
|
||||
- [ ] 实现节点增删查改:`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 API(1 天)
|
||||
|
||||
- [ ] 实现 `olive_undo_stack_*` 系列函数。
|
||||
- [ ] C API 的 undo 采用函数指针回调模式,避免暴露 C++ 的 `UndoCommand` 类。
|
||||
|
||||
### Step 5: ViewerOutput 和 Sequence(1 天)
|
||||
|
||||
- [ ] 实现 `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_frame(OliveFrame*,定义在 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_buffer(OliveSampleBuffer*,定义在 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 API(2 天)
|
||||
|
||||
- [ ] 实现 `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`。 |
|
||||
@@ -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 实现(按需,2–3 天)
|
||||
|
||||
- [ ] 实现 `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, ¶ms, &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: 集成到 ViewerWidget(2 天)
|
||||
|
||||
- [ ] 修改 `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
|
||||
// 模式 A:Create/Destroy(堆分配)
|
||||
OliveNodeGraph* olive_node_graph_create(void);
|
||||
void olive_node_graph_destroy(OliveNodeGraph* obj);
|
||||
|
||||
// 模式 B:Init/Cleanup(栈分配或外部缓冲区)
|
||||
int olive_frame_init(OliveFrame* frame, int w, int h, OlivePixelFormat fmt);
|
||||
void olive_frame_cleanup(OliveFrame* frame);
|
||||
|
||||
// 模式 C:Ref/Unref(引用计数)
|
||||
void olive_frame_ref(OliveFrame* frame);
|
||||
void olive_frame_unref(OliveFrame* frame);
|
||||
```
|
||||
|
||||
优先使用 **模式 A(Create/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** 在 CI(GitHub 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 2:libolivecore.so(1 人主责)
|
||||
|
||||
### 目标
|
||||
将 `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 3:libolivecodec.so(1 人主责)
|
||||
|
||||
### 目标
|
||||
将 `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 4:libolivenode.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 5:libolivenode.so C API + liboliveplugin.so(2 人并行)
|
||||
|
||||
### 目标
|
||||
完成节点图系统的 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 API:NodeGraph 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 6:olive-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 7:ViewerWidget 集成 + 用完即弃验证(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 8:liboliveaudio.so + liboliveui.so(2 人并行)
|
||||
|
||||
### 目标
|
||||
完成音频库和 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 人 audio,1 人 ui,1 人优化 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` | 收到 SIGINT(Ctrl+C / kill -2) | 视为取消,正常丢弃 |
|
||||
| 137 | `EXIT_SIGKILL` | 收到 SIGKILL(kill -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)
|
||||
```
|
||||
@@ -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`**:渲染子进程的通信协议细节。
|
||||
@@ -0,0 +1,258 @@
|
||||
# 渲染独立进程化 — 实现计划
|
||||
|
||||
> **状态**:实施中(阶段 0、阶段 1 已完成)
|
||||
> **分支**:`feat/render-process-isolation`
|
||||
> **范围**:把视频帧渲染拆到独立进程,主进程通过共享内存 + stdio 调度多个渲染 worker,全程无锁。
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景(为什么做)
|
||||
|
||||
Oak(Olive 分叉,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,**不**链接 UI(Widgets)。 |
|
||||
| **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 core,24 位深度),设置 `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 素材解耦完成)。
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
Reference in New Issue
Block a user