From b23e01b916c7d1f6ebebc9487fcea1dc87b3ca73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E7=BE=BD?= Date: Thu, 4 Jun 2026 13:57:36 +0800 Subject: [PATCH] Add render worker IPC loop --- app/CMakeLists.txt | 22 + app/node/project/serializer/serializer.h | 4 + .../project/serializer/serializer211228.cpp | 2 + .../project/serializer/serializer220403.cpp | 3 + .../project/serializer/serializer230220.cpp | 3 + app/render/CMakeLists.txt | 1 + app/render/ipc/CMakeLists.txt | 27 + app/render/ipc/frameslotpool.cpp | 173 ++++ app/render/ipc/frameslotpool.h | 180 ++++ app/render/ipc/ipcmessage.cpp | 195 ++++ app/render/ipc/ipcmessage.h | 143 +++ app/render/ipc/sharedmemoryregion.cpp | 205 ++++ app/render/ipc/sharedmemoryregion.h | 123 +++ app/render/ipc/spscringbuffer.h | 184 ++++ app/render/worker/workermain.cpp | 437 +++++++++ ...larization-and-multiprocess-render-plan.md | 872 ------------------ docs/zh/modularization-plan/01-olivecore.md | 466 ---------- docs/zh/modularization-plan/02-olivecodec.md | 359 ------- docs/zh/modularization-plan/03-oliveplugin.md | 305 ------ docs/zh/modularization-plan/04-oliveaudio.md | 267 ------ docs/zh/modularization-plan/05-olivenode.md | 413 --------- docs/zh/modularization-plan/06-oliverender.md | 406 -------- docs/zh/modularization-plan/07-oliveui.md | 256 ----- .../modularization-plan/08-olive-renderer.md | 692 -------------- .../zh/modularization-plan/09-c-api-design.md | 677 -------------- .../10-implementation-roadmap.md | 345 ------- .../zh/modularization-plan/11-ipc-protocol.md | 479 ---------- docs/zh/modularization-plan/README.md | 114 --- docs/zh/render-process-isolation-plan.md | 258 ++++++ tests/gtest/CMakeLists.txt | 1 + tests/gtest/project_serializer_test.cpp | 4 + tests/gtest/render_ipc_test.cpp | 381 ++++++++ 32 files changed, 2346 insertions(+), 5651 deletions(-) create mode 100644 app/render/ipc/CMakeLists.txt create mode 100644 app/render/ipc/frameslotpool.cpp create mode 100644 app/render/ipc/frameslotpool.h create mode 100644 app/render/ipc/ipcmessage.cpp create mode 100644 app/render/ipc/ipcmessage.h create mode 100644 app/render/ipc/sharedmemoryregion.cpp create mode 100644 app/render/ipc/sharedmemoryregion.h create mode 100644 app/render/ipc/spscringbuffer.h create mode 100644 app/render/worker/workermain.cpp delete mode 100644 docs/zh/modularization-and-multiprocess-render-plan.md delete mode 100644 docs/zh/modularization-plan/01-olivecore.md delete mode 100644 docs/zh/modularization-plan/02-olivecodec.md delete mode 100644 docs/zh/modularization-plan/03-oliveplugin.md delete mode 100644 docs/zh/modularization-plan/04-oliveaudio.md delete mode 100644 docs/zh/modularization-plan/05-olivenode.md delete mode 100644 docs/zh/modularization-plan/06-oliverender.md delete mode 100644 docs/zh/modularization-plan/07-oliveui.md delete mode 100644 docs/zh/modularization-plan/08-olive-renderer.md delete mode 100644 docs/zh/modularization-plan/09-c-api-design.md delete mode 100644 docs/zh/modularization-plan/10-implementation-roadmap.md delete mode 100644 docs/zh/modularization-plan/11-ipc-protocol.md delete mode 100644 docs/zh/modularization-plan/README.md create mode 100644 docs/zh/render-process-isolation-plan.md create mode 100644 tests/gtest/render_ipc_test.cpp diff --git a/app/CMakeLists.txt b/app/CMakeLists.txt index dfff99111..b8315c0ae 100644 --- a/app/CMakeLists.txt +++ b/app/CMakeLists.txt @@ -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_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) diff --git a/app/node/project/serializer/serializer.h b/app/node/project/serializer/serializer.h index b536027fe..fa311bd7e 100644 --- a/app/node/project/serializer/serializer.h +++ b/app/node/project/serializer/serializer.h @@ -83,6 +83,10 @@ public: QVector nodes; + QHash node_ptrs; + + QHash node_uuids; + Node::OutputConnections promised_connections; }; diff --git a/app/node/project/serializer/serializer211228.cpp b/app/node/project/serializer/serializer211228.cpp index 507ecd4b5..b6e29f4f3 100644 --- a/app/node/project/serializer/serializer211228.cpp +++ b/app/node/project/serializer/serializer211228.cpp @@ -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++) { diff --git a/app/node/project/serializer/serializer220403.cpp b/app/node/project/serializer/serializer220403.cpp index 70f71a94a..2f3278729 100644 --- a/app/node/project/serializer/serializer220403.cpp +++ b/app/node/project/serializer/serializer220403.cpp @@ -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()); diff --git a/app/node/project/serializer/serializer230220.cpp b/app/node/project/serializer/serializer230220.cpp index 0794e3dae..dda191cd3 100644 --- a/app/node/project/serializer/serializer230220.cpp +++ b/app/node/project/serializer/serializer230220.cpp @@ -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")) { diff --git a/app/render/CMakeLists.txt b/app/render/CMakeLists.txt index bd54ff8b7..d56e33284 100644 --- a/app/render/CMakeLists.txt +++ b/app/render/CMakeLists.txt @@ -16,6 +16,7 @@ # along with this program. If not, see . add_subdirectory(job) +add_subdirectory(ipc) add_subdirectory(ocioconf) add_subdirectory(opengl) add_subdirectory(plugin) diff --git a/app/render/ipc/CMakeLists.txt b/app/render/ipc/CMakeLists.txt new file mode 100644 index 000000000..ef1cdaaa7 --- /dev/null +++ b/app/render/ipc/CMakeLists.txt @@ -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 . + +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 +) \ No newline at end of file diff --git a/app/render/ipc/frameslotpool.cpp b/app/render/ipc/frameslotpool.cpp new file mode 100644 index 000000000..da9345e3c --- /dev/null +++ b/app/render/ipc/frameslotpool.cpp @@ -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 . + +***/ + +#include "frameslotpool.h" + +#include + +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(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
(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(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(mem); + pool.header_ = reinterpret_cast
(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(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 \ No newline at end of file diff --git a/app/render/ipc/frameslotpool.h b/app/render/ipc/frameslotpool.h new file mode 100644 index 000000000..48c48d85a --- /dev/null +++ b/app/render/ipc/frameslotpool.h @@ -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 . + +***/ + +#ifndef IPC_FRAMESLOTPOOL_H +#define IPC_FRAMESLOTPOOL_H + +#include +#include + +#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 \ No newline at end of file diff --git a/app/render/ipc/ipcmessage.cpp b/app/render/ipc/ipcmessage.cpp new file mode 100644 index 000000000..8b8822b8b --- /dev/null +++ b/app/render/ipc/ipcmessage.cpp @@ -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 . + +***/ + +#include "ipcmessage.h" + +#include +#include + +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 \ No newline at end of file diff --git a/app/render/ipc/ipcmessage.h b/app/render/ipc/ipcmessage.h new file mode 100644 index 000000000..56cb2cb83 --- /dev/null +++ b/app/render/ipc/ipcmessage.h @@ -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 . + +***/ + +#ifndef IPC_IPCMESSAGE_H +#define IPC_IPCMESSAGE_H + +#include +#include +#include +#include + +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 \ No newline at end of file diff --git a/app/render/ipc/sharedmemoryregion.cpp b/app/render/ipc/sharedmemoryregion.cpp new file mode 100644 index 000000000..ee6e6a8cd --- /dev/null +++ b/app/render/ipc/sharedmemoryregion.cpp @@ -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 . + +***/ + +#include "sharedmemoryregion.h" + +#include + +#if defined(Q_OS_WIN) +#include +#else +#include +#include +#include +#include +#include +#include +#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((quint64(size) >> 32) & 0xFFFFFFFF); + const DWORD size_low = static_cast(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 \ No newline at end of file diff --git a/app/render/ipc/sharedmemoryregion.h b/app/render/ipc/sharedmemoryregion.h new file mode 100644 index 000000000..2a867284e --- /dev/null +++ b/app/render/ipc/sharedmemoryregion.h @@ -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 . + +***/ + +#ifndef IPC_SHAREDMEMORYREGION_H +#define IPC_SHAREDMEMORYREGION_H + +#include +#include + +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--". + * + * 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 \ No newline at end of file diff --git a/app/render/ipc/spscringbuffer.h b/app/render/ipc/spscringbuffer.h new file mode 100644 index 000000000..a6e62eeb1 --- /dev/null +++ b/app/render/ipc/spscringbuffer.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 . + +***/ + +#ifndef IPC_SPSCRINGBUFFER_H +#define IPC_SPSCRINGBUFFER_H + +#include +#include +#include + +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(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(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(this + 1); + } + const uint32_t *slot_array() const + { + return reinterpret_cast(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 head_; + std::atomic tail_; + uint32_t capacity_; + + static_assert(sizeof(std::atomic) == sizeof(uint32_t), + "atomic must be lock-free POD-sized for shared memory use"); +}; + +} // namespace ipc +} // namespace olive + +#endif // IPC_SPSCRINGBUFFER_H diff --git a/app/render/worker/workermain.cpp b/app/render/worker/workermain.cpp new file mode 100644 index 000000000..66039abaf --- /dev/null +++ b/app/render/worker/workermain.cpp @@ -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 . + +***/ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#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(); + 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(); + 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(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(); + 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 project_; + QHash node_by_token_; + olive::ipc::SharedMemoryRegion output_region_; + std::optional 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; +} diff --git a/docs/zh/modularization-and-multiprocess-render-plan.md b/docs/zh/modularization-and-multiprocess-render-plan.md deleted file mode 100644 index cd3bea941..000000000 --- a/docs/zh/modularization-and-multiprocess-render-plan.md +++ /dev/null @@ -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__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 - $ - $ -) - -# 方案 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_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":"........."} - {"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::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 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_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; - }>; -} - -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 层无需感知底层架构变化。 diff --git a/docs/zh/modularization-plan/01-olivecore.md b/docs/zh/modularization-plan/01-olivecore.md deleted file mode 100644 index 4dbb83e61..000000000 --- a/docs/zh/modularization-plan/01-olivecore.md +++ /dev/null @@ -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 -#include - -#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 -#include -#include -#include -#include -#include -#include -#include -#include - -// 线程局部错误状态 -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(display_mode) - ); - QByteArray utf8 = str.toUtf8(); - char* result = static_cast(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 - $ - $ - 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("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 -``` diff --git a/docs/zh/modularization-plan/02-olivecodec.md b/docs/zh/modularization-plan/02-olivecodec.md deleted file mode 100644 index 21ee449a1..000000000 --- a/docs/zh/modularization-plan/02-olivecodec.md +++ /dev/null @@ -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 - $ -) - -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`。 | diff --git a/docs/zh/modularization-plan/03-oliveplugin.md b/docs/zh/modularization-plan/03-oliveplugin.md deleted file mode 100644 index dd896dd90..000000000 --- a/docs/zh/modularization-plan/03-oliveplugin.md +++ /dev/null @@ -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 - $ -) - -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 实例句柄,状态完全隔离。 | diff --git a/docs/zh/modularization-plan/04-oliveaudio.md b/docs/zh/modularization-plan/04-oliveaudio.md deleted file mode 100644 index bb4100fab..000000000 --- a/docs/zh/modularization-plan/04-oliveaudio.md +++ /dev/null @@ -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 - $ -) - -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` 中完成。 | diff --git a/docs/zh/modularization-plan/05-olivenode.md b/docs/zh/modularization-plan/05-olivenode.md deleted file mode 100644 index e70893e98..000000000 --- a/docs/zh/modularization-plan/05-olivenode.md +++ /dev/null @@ -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 - $ -) - -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` 的文件路径设置/获取。 | diff --git a/docs/zh/modularization-plan/06-oliverender.md b/docs/zh/modularization-plan/06-oliverender.md deleted file mode 100644 index 21b83f729..000000000 --- a/docs/zh/modularization-plan/06-oliverender.md +++ /dev/null @@ -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(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(); - 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(); - 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 - $ -) - -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`。 | diff --git a/docs/zh/modularization-plan/07-oliveui.md b/docs/zh/modularization-plan/07-oliveui.md deleted file mode 100644 index a0eef662f..000000000 --- a/docs/zh/modularization-plan/07-oliveui.md +++ /dev/null @@ -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 - $ -) - -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。 | diff --git a/docs/zh/modularization-plan/08-olive-renderer.md b/docs/zh/modularization-plan/08-olive-renderer.md deleted file mode 100644 index d6a7c573a..000000000 --- a/docs/zh/modularization-plan/08-olive-renderer.md +++ /dev/null @@ -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= 渲染后端:opengl(默认), dummy - --shader-path= 着色器资源目录(默认:可执行文件同级目录下的 shaders/) - --ocio-config= OpenColorIO 配置文件路径 - --verbose 输出详细日志到 stderr - -单帧模式(--mode=frame): - --mode=frame - --node-graph= 节点图 XML 文件路径 - --output-node= 输出节点 ID(默认:图中的第一个 ViewerOutput) - --time= 渲染时间点,如 "1001/30000" - --video-params= 视频参数 JSON,如 '{"width":1920,"height":1080,"format":"rgba32f"}' - --audio-params= 音频参数 JSON - --color-ref= 参考色彩空间 - --color-display= 显示色彩空间 - --output-shm= POSIX 共享内存名称(如 "/olive_r_123")或临时文件路径 - --output-shm-size= 共享内存大小 - --output-stdout 将帧数据 base64 编码输出到 stdout(仅小帧/测试用) - -批处理模式(--mode=batch): - --mode=batch - --node-graph= - --output-node= - --times= 逗号分隔的时间点列表,如 "0/30,1/30,2/30" - --video-params= - --audio-params= - --output-shm= - --output-shm-size= - -音频模式(--mode=audio): - --mode=audio - --node-graph= - --output-node= - --start= 起始时间 - --duration= 持续时间 - --audio-params= - --output-shm= - --output-shm-size= -``` - -### 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 -#include -#include -#include -#include -#include -#include -#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(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 - -#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 -#include -#include -#include -#include - -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 - -void* olive_shm_create(const char* name, size_t size) { - HANDLE hMap = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, - PAGE_READWRITE, 0, static_cast(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 -#include -#include -#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(viewer_output_)), - time, - video_params_, - audio_params_, - OLIVE_RENDER_MODE_ONLINE - ); - - // 4. 连接完成信号 - connect(proc, QOverload::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__`。 | -| 共享内存泄漏(子进程崩溃后未 unlink) | 主进程在启动子进程前注册一个定时器,若子进程异常退出,5 秒后自动 `shm_unlink`。 | -| 磁盘空间不足(临时文件方案) | 渲染前检查磁盘空间,不足时返回错误码 6。 | -| 节点图 XML 过大导致解析慢 | 启用增量序列化:只序列化自上次以来变更的节点和参数。 | -| OFX 插件需要持久化状态 | OFX 插件实例不跨帧持久化,每帧重新创建。若某些插件初始化极慢,在 C API 中提供 `olive_plugin_instance_serialize_state` 接口,将状态快照传给下一帧的进程。 | diff --git a/docs/zh/modularization-plan/09-c-api-design.md b/docs/zh/modularization-plan/09-c-api-design.md deleted file mode 100644 index 03766efc6..000000000 --- a/docs/zh/modularization-plan/09-c-api-design.md +++ /dev/null @@ -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__`。 -- 参数和返回值仅使用 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__` | `olive_node_graph_create`, `olive_codec_decoder_open` | -| 枚举名 | `Olive` | `OliveCodecResultOk`, `OliveRenderModeOffline` | -| 常量宏 | `OLIVE__UPPER_SNAKE` | `OLIVE_NODE_OK`, `OLIVE_CODEC_ERROR_NOT_FOUND` | -| 版本宏 | `OLIVE__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 -#include -#include - -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 - FuncPtr GetFunction(const QString& module_name, const char* func_name) { - return reinterpret_cast(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 modules_; - QString last_error_; -}; - -// 便捷宏:从指定模块获取函数并调用 -#define OLIVE_LOAD_FUNC(loader, module, name, type) \ - auto name = (loader).GetFunction(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 -#include - -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 - -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(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("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 - $ -) - -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 -#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()`。 | diff --git a/docs/zh/modularization-plan/10-implementation-roadmap.md b/docs/zh/modularization-plan/10-implementation-roadmap.md deleted file mode 100644 index 12a5e9eb1..000000000 --- a/docs/zh/modularization-plan/10-implementation-roadmap.md +++ /dev/null @@ -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("core", "olive_rational_make"); -auto add = loader.GetFunction("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` 多进程(核心价值),动态库拆分可以延后。 | diff --git a/docs/zh/modularization-plan/11-ipc-protocol.md b/docs/zh/modularization-plan/11-ipc-protocol.md deleted file mode 100644 index 78838d7f2..000000000 --- a/docs/zh/modularization-plan/11-ipc-protocol.md +++ /dev/null @@ -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 | 否 | `/shaders` | 着色器资源目录 | -| `--ocio-config` | path | 否 | — | OCIO 配置文件路径 | -| `--verbose` | flag | 否 | false | 详细日志输出到 stderr | -| `--version` | flag | 否 | false | 输出版本信息并退出 | -| `--help` | flag | 否 | false | 输出帮助信息并退出 | - -### 2.2 参数值格式 - -**Rational**:`"/"`,如 `"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 - -#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_`,必须以 `/` 开头,长度 < 255。 -- Windows: `Local\\olive_` 或 `Global\\olive_`。 -- 临时文件: `/tmp/olive__.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_`(用户会话内可见)。 -- 注意:句柄管理。子进程 `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) -``` diff --git a/docs/zh/modularization-plan/README.md b/docs/zh/modularization-plan/README.md deleted file mode 100644 index cdd804315..000000000 --- a/docs/zh/modularization-plan/README.md +++ /dev/null @@ -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/_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`**:渲染子进程的通信协议细节。 diff --git a/docs/zh/render-process-isolation-plan.md b/docs/zh/render-process-isolation-plan.md new file mode 100644 index 000000000..c1b1016a4 --- /dev/null +++ b/docs/zh/render-process-isolation-plan.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` 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 素材解耦完成)。 diff --git a/tests/gtest/CMakeLists.txt b/tests/gtest/CMakeLists.txt index 4080ef151..00fa941b8 100644 --- a/tests/gtest/CMakeLists.txt +++ b/tests/gtest/CMakeLists.txt @@ -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 diff --git a/tests/gtest/project_serializer_test.cpp b/tests/gtest/project_serializer_test.cpp index b08203f0a..d897f14b3 100644 --- a/tests/gtest/project_serializer_test.cpp +++ b/tests/gtest/project_serializer_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(node))); + EXPECT_TRUE(loaded_project.nodes().contains( + result.GetLoadData().node_ptrs.value(reinterpret_cast(node)))); olive::ProjectSerializer::Destroy(); if (created_disk_manager) { diff --git a/tests/gtest/render_ipc_test.cpp b/tests/gtest/render_ipc_test.cpp new file mode 100644 index 000000000..75a36ba14 --- /dev/null +++ b/tests/gtest/render_ipc_test.cpp @@ -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 + +#include +#include +#include +#include + +#include +#include +#include + +#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 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 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 mem(SpscRingBuffer::BytesNeeded(kCapacity)); + SpscRingBuffer *ring = SpscRingBuffer::Create(mem.data(), kCapacity); + + std::atomic 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 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(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(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 mem(FrameSlotPool::BytesNeeded(kSlots, kSlotBytes)); + FrameSlotPool pool = FrameSlotPool::Create(mem.data(), kSlots, kSlotBytes); + + // Acquire every slot, then confirm the pool reports empty. + std::vector 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 mem(FrameSlotPool::BytesNeeded(kSlots, kSlotBytes)); + FrameSlotPool filler = FrameSlotPool::Create(mem.data(), kSlots, kSlotBytes); + FrameSlotPool drainer = FrameSlotPool::Attach(mem.data()); + + std::atomic 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(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(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)); +} \ No newline at end of file