Files
oak-editor/app/render/worker/workermain.cpp
T
Mike-Solar 225f8505c2 feat(render): dynamic OpenGL/Vulkan backend split and backend-neutral viewer
- Extract libolive-rendercore static library to minimize backend link boundary.
- Add DynamicRenderer adapter with C ABI (oakgl/oakvulkan shared libs).
- Make OAK_ENABLE_DYNAMIC_RENDER_BACKEND default ON with OpenGL fallback.
- Implement VulkanRenderer prototype (textures, shaders, UBO blit, readback).
- Add backend-neutral viewer readback path (offscreen -> QImage -> QPainter).
- Refactor PluginRenderer to be renderer-agnostic; OFX plugins fall back to CPU
  path on non-OpenGL backends while preserving OpenGL render path.
- Add Renderer::AttachOutputTexture/DetachOutputTexture and C ABI forwards.
- Update docs/zh/render-backend-dynamic-plan.md for Phase 3/4/5.
2026-07-13 10:19:29 +08:00

560 lines
17 KiB
C++

/***
Oak - Non-Linear Video Editor
Copyright (C) 2026 Oak Team
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
***/
#include <cstdio>
#include <cstring>
#include <memory>
#include <optional>
#include <QFile>
#include <QGuiApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QMatrix4x4>
#include <QOpenGLContext>
#include <QSurfaceFormat>
#include "common/qtutils.h"
#include "config/config.h"
#include "node/factory.h"
#include "node/input/multicam/multicamnode.h"
#include "node/project/serializer/serializer.h"
#include "render/ipc/frameslotpool.h"
#include "render/ipc/ipcmessage.h"
#include "render/ipc/sharedmemoryregion.h"
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
#include "render/backend/dynamicrenderer.h"
#endif
#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::Renderer *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();
QOpenGLContext *ctx = nullptr;
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
if (auto *dynamic_renderer = dynamic_cast<olive::DynamicRenderer *>(renderer_)) {
ctx = dynamic_renderer->OpenGLContext();
} else
#endif
{
ctx = static_cast<olive::OpenGLRenderer *>(renderer_)->context();
}
if (ctx) {
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<olive::Project>();
loaded->Initialize();
olive::ProjectSerializer::Result result =
olive::ProjectSerializer::Load(loaded.get(), path, olive::ProjectSerializer::kProject);
if (result != olive::ProjectSerializer::kSuccess) {
return Write(ErrorMessage(QStringLiteral("failed to load graph %1: %2")
.arg(path, result.GetDetails())));
}
project_ = std::move(loaded);
node_by_token_.clear();
const auto &data = result.GetLoadData();
for (auto it = data.node_ptrs.cbegin(); it != data.node_ptrs.cend(); ++it) {
node_by_token_.insert(QString::number(it.key()), it.value());
}
for (auto it = data.node_uuids.cbegin(); it != data.node_uuids.cend(); ++it) {
node_by_token_.insert(it.value().toString(), it.key());
node_by_token_.insert(it.value().toString(QUuid::WithoutBraces), it.key());
}
QJsonObject ack;
ack["type"] = QStringLiteral("graph_loaded");
ack["nodes"] = node_by_token_.size();
return Write(ack);
}
olive::Node *FindNode(const QString &token) const
{
if (olive::Node *node = node_by_token_.value(token, nullptr)) {
return node;
}
bool ok = false;
const quintptr ptr = token.toULongLong(&ok, 0);
if (ok) {
return node_by_token_.value(QString::number(ptr), nullptr);
}
return nullptr;
}
bool RenderFrame(const olive::ipc::RenderFrameMsg &message)
{
if (!project_) {
return Write(ErrorMessage(QStringLiteral("render_frame received before load_graph"),
message.ticket_id));
}
if (!output_pool_ || !output_pool_->IsValid()) {
return Write(ErrorMessage(QStringLiteral("render_frame received before output shm handshake"),
message.ticket_id));
}
olive::Node *node = FindNode(message.node_uuid);
if (!node) {
return Write(ErrorMessage(QStringLiteral("render node not found: %1").arg(message.node_uuid),
message.ticket_id));
}
QVector<int> input_slots;
const QVector<int> requested_input_slots =
message.input_slots.isEmpty() && message.input_slot >= 0
? QVector<int>{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) {
if (requested_slot < 0 ||
requested_slot >= int(input_pool_->slot_count())) {
for (int slot : input_slots) {
input_pool_->Release(uint32_t(slot));
}
return Write(ErrorMessage(QStringLiteral("input slot index out of range"),
message.ticket_id));
}
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<olive::RenderTicket>();
ticket->setProperty("node", olive::QtUtils::PtrToValue(node));
ticket->setProperty("time", QVariant::fromValue(
olive::rational(int(message.time_num), int(message.time_den))));
ticket->setProperty("size", QSize(message.width, message.height));
ticket->setProperty("matrix", QMatrix4x4());
ticket->setProperty("format",
message.format >= 0
? olive::PixelFormat::Format(message.format)
: olive::PixelFormat::INVALID);
ticket->setProperty("usecache", false);
ticket->setProperty("channelcount", message.channel_count);
ticket->setProperty("mode", olive::RenderMode::Mode(message.mode));
ticket->setProperty("type", olive::RenderManager::kTypeVideo);
ticket->setProperty("colormanager", olive::QtUtils::PtrToValue(project_->color_manager()));
ticket->setProperty("coloroutput", QVariant::fromValue(olive::ColorProcessorPtr()));
ticket->setProperty("vparam", QVariant::fromValue(vparams));
ticket->setProperty("aparam", QVariant::fromValue(olive::AudioParams()));
ticket->setProperty("return", olive::RenderManager::kFrame);
ticket->setProperty("cache", QString());
ticket->setProperty("cachetimebase", QVariant::fromValue(olive::rational(1)));
ticket->setProperty("cacheid", QVariant::fromValue(QUuid()));
ticket->setProperty("multicam", olive::QtUtils::PtrToValue(static_cast<void *>(nullptr)));
ticket->setProperty("ipc_input_pool",
olive::QtUtils::PtrToValue(
input_pool_ ? static_cast<void *>(&*input_pool_)
: static_cast<void *>(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<olive::FramePtr>();
if (!frame || !frame->is_allocated()) {
return Write(ErrorMessage(QStringLiteral("render result was empty"), message.ticket_id));
}
uint32_t slot = 0;
if (!output_pool_->Acquire(&slot)) {
return Write(ErrorMessage(QStringLiteral("no free output frame slot"), message.ticket_id));
}
const int data_size = frame->allocated_size();
if (data_size > int(output_pool_->slot_data_bytes())) {
output_pool_->Release(slot);
return Write(ErrorMessage(QStringLiteral("rendered frame does not fit output slot"),
message.ticket_id));
}
std::memcpy(output_pool_->SlotData(slot), frame->const_data(), size_t(data_size));
olive::ipc::FrameSlotMeta *meta = output_pool_->Meta(slot);
meta->id = message.ticket_id;
meta->time_num = frame->timestamp().numerator();
meta->time_den = frame->timestamp().denominator();
meta->width = frame->width();
meta->height = frame->height();
meta->format = int32_t(frame->format());
meta->channel_count = frame->channel_count();
meta->linesize = frame->linesize_bytes();
meta->data_size = data_size;
if (!output_pool_->Publish(slot)) {
output_pool_->Release(slot);
return Write(ErrorMessage(QStringLiteral("failed to publish output frame slot"),
message.ticket_id));
}
olive::ipc::FrameReadyMsg ready;
ready.ticket_id = message.ticket_id;
ready.output_slot = int(slot);
return Write(ready.ToJson());
}
olive::Renderer *renderer_;
QFile *out_;
bool shutdown_requested_ = false;
std::unique_ptr<olive::Project> project_;
QHash<QString, olive::Node *> node_by_token_;
olive::ipc::SharedMemoryRegion output_region_;
std::optional<olive::ipc::FrameSlotPool> output_pool_;
olive::ipc::SharedMemoryRegion input_region_;
std::optional<olive::ipc::FrameSlotPool> 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;
}
olive::Renderer *renderer;
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
auto *dynamic_renderer = new olive::DynamicRenderer(QStringLiteral("opengl"));
if (dynamic_renderer->Init()) {
dynamic_renderer->PostInit();
renderer = dynamic_renderer;
} else {
delete dynamic_renderer;
qWarning() << "Failed to initialize dynamic OpenGL backend, falling back to direct OpenGL renderer";
renderer = new olive::OpenGLRenderer();
if (!renderer->Init()) {
LogError(QStringLiteral("failed to initialize OpenGL renderer"));
delete renderer;
return 1;
}
renderer->PostInit();
}
#else
renderer = new olive::OpenGLRenderer();
if (!renderer->Init()) {
LogError(QStringLiteral("failed to initialize OpenGL renderer"));
delete renderer;
return 1;
}
renderer->PostInit();
#endif
QOpenGLContext *ctx = nullptr;
#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND
if (auto *loaded_renderer = dynamic_cast<olive::DynamicRenderer *>(renderer)) {
ctx = loaded_renderer->OpenGLContext();
} else
#endif
{
ctx = static_cast<olive::OpenGLRenderer *>(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;
}