/*** 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_shm_key = QString(); hs.input_slots = 0; hs.output_slots = 0; hs.slot_data_bytes = 0; hs.input_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"))); } input_pool_.reset(); input_region_.Close(); if (hs.input_slots > 0) { if (hs.input_shm_key.isEmpty() || hs.input_slot_data_bytes <= 0) { return Write(ErrorMessage(QStringLiteral("handshake missing input shared-memory geometry"))); } const size_t input_bytes = olive::ipc::FrameSlotPool::BytesNeeded( uint32_t(hs.input_slots), size_t(hs.input_slot_data_bytes)); if (!input_region_.Open(hs.input_shm_key, input_bytes, olive::ipc::SharedMemoryRegion::kAttach)) { return Write(ErrorMessage(QStringLiteral("failed to attach input shared memory: %1") .arg(input_region_.error()))); } input_pool_ = olive::ipc::FrameSlotPool::Attach(input_region_.data()); if (!input_pool_->IsValid()) { input_region_.Close(); input_pool_.reset(); return Write(ErrorMessage(QStringLiteral("input 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)); } QVector input_slots; const QVector requested_input_slots = message.input_slots.isEmpty() && message.input_slot >= 0 ? QVector{message.input_slot} : message.input_slots; if (!requested_input_slots.isEmpty()) { if (!input_pool_ || !input_pool_->IsValid()) { return Write(ErrorMessage(QStringLiteral("render_frame referenced input slot without input pool"), message.ticket_id)); } for (int requested_slot : requested_input_slots) { uint32_t consumed_slot = 0; if (!input_pool_->Consume(&consumed_slot)) { for (int slot : input_slots) { input_pool_->Release(uint32_t(slot)); } return Write(ErrorMessage(QStringLiteral("input slot was not ready"), message.ticket_id)); } if (int(consumed_slot) != requested_slot) { input_pool_->Release(consumed_slot); for (int slot : input_slots) { input_pool_->Release(uint32_t(slot)); } return Write(ErrorMessage(QStringLiteral("input slot order mismatch"), message.ticket_id)); } input_slots.append(int(consumed_slot)); } } 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->setProperty("ipc_input_pool", olive::QtUtils::PtrToValue( input_pool_ ? static_cast(&*input_pool_) : static_cast(nullptr))); QVariantList input_slot_values; for (int slot : input_slots) { input_slot_values.append(slot); } ticket->setProperty("ipc_input_slots", input_slot_values); ticket->setProperty("ipc_input_slot_cursor", 0); ticket->setProperty("ipc_input_slot", input_slots.isEmpty() ? -1 : input_slots.front()); ticket->Start(); olive::RenderProcessor::Process(ticket, renderer_, nullptr, &shader_cache_); for (int slot : input_slots) { input_pool_->Release(uint32_t(slot)); } 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::ipc::SharedMemoryRegion input_region_; std::optional input_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; }