From d66779d28177c9fdccb7d6b45eab42faeaf212b1 Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Sun, 12 Jul 2026 22:15:21 +0800 Subject: [PATCH] Apply export color transform in render worker The export color transform was passed to RenderTask as a ColorProcessor, but the worker IPC render_frame message never carried it. The worker always set the ticket's coloroutput to null, so frames were returned in the project's reference space and encoded without the chosen output transform (e.g. Rec.709 / sRGB), causing the exported video to look wrongly tinted. Serialize the ColorTransform through the render_frame control message and reconstruct the ColorProcessor on the worker side before rendering. Also expose ColorTransform as a Qt metatype so it can be stored in a ticket QVariant. --- app/render/colortransform.h | 2 ++ app/render/ipc/ipcmessage.cpp | 16 ++++++++++++++++ app/render/ipc/ipcmessage.h | 8 ++++++++ app/render/rendermanager.cpp | 2 ++ app/render/rendermanager.h | 3 +++ app/render/renderworkerpool.cpp | 7 +++++++ app/render/worker/workermain.cpp | 23 ++++++++++++++++++++++- app/task/export/export.cpp | 2 +- app/task/render/render.cpp | 12 ++++++++---- app/task/render/render.h | 7 ++++--- 10 files changed, 73 insertions(+), 9 deletions(-) diff --git a/app/render/colortransform.h b/app/render/colortransform.h index 3598d3b5e..703bcf0e6 100644 --- a/app/render/colortransform.h +++ b/app/render/colortransform.h @@ -87,4 +87,6 @@ private: } +Q_DECLARE_METATYPE(olive::ColorTransform) + #endif // COLORTRANSFORM_H diff --git a/app/render/ipc/ipcmessage.cpp b/app/render/ipc/ipcmessage.cpp index 899620da7..f8790ae68 100644 --- a/app/render/ipc/ipcmessage.cpp +++ b/app/render/ipc/ipcmessage.cpp @@ -123,6 +123,14 @@ QJsonObject RenderFrameMsg::ToJson() const input_slot_array.append(slot); } o["input_slots"] = input_slot_array; + + if (has_color_transform) { + o["has_color_transform"] = true; + o["color_is_display"] = color_is_display; + o["color_output"] = color_output; + o["color_view"] = color_view; + o["color_look"] = color_look; + } return o; } @@ -149,6 +157,14 @@ bool RenderFrameMsg::FromJson(const QJsonObject &o, RenderFrameMsg *out) if (out->input_slots.isEmpty() && out->input_slot >= 0) { out->input_slots.append(out->input_slot); } + + out->has_color_transform = o["has_color_transform"].toBool(false); + if (out->has_color_transform) { + out->color_is_display = o["color_is_display"].toBool(false); + out->color_output = o["color_output"].toString(); + out->color_view = o["color_view"].toString(); + out->color_look = o["color_look"].toString(); + } return true; } diff --git a/app/render/ipc/ipcmessage.h b/app/render/ipc/ipcmessage.h index 6a581460f..316088c27 100644 --- a/app/render/ipc/ipcmessage.h +++ b/app/render/ipc/ipcmessage.h @@ -116,6 +116,14 @@ struct RenderFrameMsg { int input_slot = -1; ///< Optional main->worker decoded input slot for footage nodes. QVector input_slots; ///< Optional ordered decoded input slots for footage nodes. + // Output color transform to apply before returning the frame. When empty, + // the worker returns the image in the project's reference space. + bool has_color_transform = false; + bool color_is_display = false; + QString color_output; + QString color_view; + QString color_look; + QJsonObject ToJson() const; static bool FromJson(const QJsonObject &o, RenderFrameMsg *out); }; diff --git a/app/render/rendermanager.cpp b/app/render/rendermanager.cpp index 14cd0c3de..bbe62f7a4 100644 --- a/app/render/rendermanager.cpp +++ b/app/render/rendermanager.cpp @@ -196,6 +196,8 @@ RenderTicketPtr RenderManager::RenderFrame(const RenderVideoParams ¶ms) QtUtils::PtrToValue(params.color_manager)); ticket->setProperty("coloroutput", QVariant::fromValue(params.force_color_output)); + ticket->setProperty("colortransform", + QVariant::fromValue(params.force_color_transform)); Q_ASSERT(params.video_params.is_valid()); ticket->setProperty("vparam", QVariant::fromValue(params.video_params)); ticket->setProperty("aparam", QVariant::fromValue(params.audio_params)); diff --git a/app/render/rendermanager.h b/app/render/rendermanager.h index a701007e4..33d40f2ff 100644 --- a/app/render/rendermanager.h +++ b/app/render/rendermanager.h @@ -32,6 +32,7 @@ #include "node/traverser.h" #include "render/previewautocacher.h" #include "render/renderer.h" +#include "render/colortransform.h" #include "render/renderticket.h" #include "rendercache.h" @@ -120,6 +121,7 @@ public: return_type = kFrame; force_format = PixelFormat::INVALID; force_color_output = nullptr; + force_color_transform = ColorTransform(); force_size = QSize(0, 0); force_channel_count = 0; mode = m; @@ -152,6 +154,7 @@ public: QMatrix4x4 force_matrix; PixelFormat force_format; ColorProcessorPtr force_color_output; + ColorTransform force_color_transform; }; static const rational kDryRunInterval; diff --git a/app/render/renderworkerpool.cpp b/app/render/renderworkerpool.cpp index 79331f50b..fceb393f1 100644 --- a/app/render/renderworkerpool.cpp +++ b/app/render/renderworkerpool.cpp @@ -941,6 +941,13 @@ RenderWorkerPool::JobResult RenderWorkerPool::ProcessJobAttempt( render.input_slot = input_slots.isEmpty() ? -1 : input_slots.front(); render.input_slots = input_slots; + const ColorTransform &ct = job.params.force_color_transform; + render.has_color_transform = !ct.output().isEmpty(); + render.color_is_display = ct.is_display(); + render.color_output = ct.output(); + render.color_view = ct.view(); + render.color_look = ct.look(); + if (!WriteControlMessage(worker->process, render.ToJson())) { if (!job.ticket->IsCancelled()) { qWarning() << "RenderWorkerPool failed to send render_frame"; diff --git a/app/render/worker/workermain.cpp b/app/render/worker/workermain.cpp index 1e8ac70e9..2fcfc2aee 100644 --- a/app/render/worker/workermain.cpp +++ b/app/render/worker/workermain.cpp @@ -48,6 +48,8 @@ #include "render/opengl/openglrenderer.h" #include "render/rendermanager.h" #include "render/renderprocessor.h" +#include "render/colorprocessor.h" +#include "render/colortransform.h" namespace { @@ -390,7 +392,26 @@ private: 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())); + + { + olive::ColorProcessorPtr color_output; + if (message.has_color_transform) { + olive::ColorTransform transform; + if (message.color_is_display) { + transform = olive::ColorTransform(message.color_output, + message.color_view, + message.color_look); + } else { + transform = olive::ColorTransform(message.color_output); + } + color_output = olive::ColorProcessor::Create( + project_->color_manager(), + project_->color_manager()->GetReferenceColorSpace(), + transform); + } + ticket->setProperty("coloroutput", + QVariant::fromValue(color_output)); + } ticket->setProperty("vparam", QVariant::fromValue(vparams)); ticket->setProperty("aparam", QVariant::fromValue(olive::AudioParams())); ticket->setProperty("return", olive::RenderManager::kFrame); diff --git a/app/task/export/export.cpp b/app/task/export/export.cpp index 42c054204..e499e93ab 100644 --- a/app/task/export/export.cpp +++ b/app/task/export/export.cpp @@ -173,7 +173,7 @@ bool ExportTask::Run() Render(color_manager_, video_range, audio_range, subtitle_range, RenderMode::kOnline, nullptr, video_force_size, video_force_matrix, encoder_->GetDesiredPixelFormat(), VideoParams::kRGBAChannelCount, - color_processor_); + color_processor_, params_.color_transform()); bool success = true; diff --git a/app/task/render/render.cpp b/app/task/render/render.cpp index 1ebcf6fe4..593d1adc9 100644 --- a/app/task/render/render.cpp +++ b/app/task/render/render.cpp @@ -43,7 +43,8 @@ bool RenderTask::Render(ColorManager *manager, const TimeRangeList &video_range, FrameHashCache *cache, const QSize &force_size, const QMatrix4x4 &force_matrix, PixelFormat force_format, int force_channel_count, - ColorProcessorPtr force_color_output) + ColorProcessorPtr force_color_output, + const ColorTransform &force_color_transform) { QMetaObject::invokeMethod(RenderManager::instance(), "SetAggressiveGarbageCollection", @@ -91,7 +92,7 @@ bool RenderTask::Render(ColorManager *manager, const TimeRangeList &video_range, i < maximum_rendered_frames && iterator.GetNext(&next_frame); i++) { StartTicket(&watcher_thread, manager, next_frame, mode, cache, force_size, force_matrix, force_format, force_channel_count, - force_color_output); + force_color_output, force_color_transform); } bool result = true; @@ -224,7 +225,8 @@ bool RenderTask::Render(ColorManager *manager, const TimeRangeList &video_range, if (iterator.GetNext(&next_frame)) { StartTicket(&watcher_thread, manager, next_frame, mode, cache, force_size, force_matrix, force_format, - force_channel_count, force_color_output); + force_channel_count, force_color_output, + force_color_transform); } } @@ -313,7 +315,8 @@ void RenderTask::StartTicket(QThread *watcher_thread, ColorManager *manager, FrameHashCache *cache, const QSize &force_size, const QMatrix4x4 &force_matrix, PixelFormat force_format, int force_channel_count, - ColorProcessorPtr force_color_output) + ColorProcessorPtr force_color_output, + const ColorTransform &force_color_transform) { RenderManager::RenderVideoParams rvp(viewer_->GetConnectedTextureOutput(), video_params_, audio_params_, time, @@ -323,6 +326,7 @@ void RenderTask::StartTicket(QThread *watcher_thread, ColorManager *manager, rvp.force_matrix = force_matrix; rvp.force_format = force_format; rvp.force_color_output = force_color_output; + rvp.force_color_transform = force_color_transform; rvp.force_channel_count = force_channel_count; if (cache) { diff --git a/app/task/render/render.h b/app/task/render/render.h index 51a589661..f943a243b 100644 --- a/app/task/render/render.h +++ b/app/task/render/render.h @@ -48,7 +48,8 @@ protected: const QMatrix4x4 &force_matrix = QMatrix4x4(), PixelFormat force_format = PixelFormat::INVALID, int force_channel_count = 0, - ColorProcessorPtr force_color_output = nullptr); + ColorProcessorPtr force_color_output = nullptr, + const ColorTransform &force_color_transform = ColorTransform()); virtual bool DownloadFrame(QThread *thread, FramePtr frame, const rational &time); @@ -124,8 +125,8 @@ private: const rational &time, RenderMode::Mode mode, FrameHashCache *cache, const QSize &force_size, const QMatrix4x4 &force_matrix, PixelFormat force_format, - int force_channel_count, - ColorProcessorPtr force_color_output); + int force_channel_count, ColorProcessorPtr force_color_output, + const ColorTransform &force_color_transform); ViewerOutput *viewer_;