diff --git a/tests/gtest/dialog_editing_test.cpp b/tests/gtest/dialog_editing_test.cpp index 70865f7cd..36eba100c 100644 --- a/tests/gtest/dialog_editing_test.cpp +++ b/tests/gtest/dialog_editing_test.cpp @@ -46,7 +46,7 @@ namespace void ensure_app_singletons() { if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } if (!olive::DiskManager::instance()) { olive::DiskManager::create_instance(); diff --git a/tests/gtest/dialog_export_test.cpp b/tests/gtest/dialog_export_test.cpp index 5ce26eec7..691faa210 100644 --- a/tests/gtest/dialog_export_test.cpp +++ b/tests/gtest/dialog_export_test.cpp @@ -8,6 +8,7 @@ #include #include "codec/encoder.h" +#include "oakengine/encoding.h" #include "dialog/export/codec/h264section.h" #include "dialog/export/codec/imagesection.h" #include "dialog/export/exportadvancedvideodialog.h" @@ -18,6 +19,7 @@ #include "dialog/export/exportvideotab.h" #include "node/color/colormanager/colormanager.h" #include "node/project.h" +#include "widget/manageddisplay/colorprocessorhandle.h" namespace { @@ -195,7 +197,7 @@ TEST(DialogExportVideoTab, SetFormatPopulatesCodecs) olive::ColorManager::set_up_default_config(); olive::Project project; - olive::ExportVideoTab tab(project.color_manager()); + olive::ExportVideoTab tab(oak_color_manager(project.color_manager())); const QList codecs = olive::ExportFormat::get_video_codecs(olive::ExportFormat::k_format_matroska); @@ -214,7 +216,7 @@ TEST(DialogExportVideoTab, CodecSelectsMatchingSection) olive::ColorManager::set_up_default_config(); olive::Project project; - olive::ExportVideoTab tab(project.color_manager()); + olive::ExportVideoTab tab(oak_color_manager(project.color_manager())); tab.set_format(olive::ExportFormat::k_format_matroska); // First Matroska codec is H.264, which has a dedicated section @@ -233,7 +235,7 @@ TEST(DialogExportVideoTab, ImageSequenceCheckboxRoundTrips) olive::ColorManager::set_up_default_config(); olive::Project project; - olive::ExportVideoTab tab(project.color_manager()); + olive::ExportVideoTab tab(oak_color_manager(project.color_manager())); tab.set_format(olive::ExportFormat::k_format_png); tab.video_codec_changed(); @@ -249,7 +251,7 @@ TEST(DialogExportVideoTab, MaintainAspectTogglesScalingMethod) olive::ColorManager::set_up_default_config(); olive::Project project; - olive::ExportVideoTab tab(project.color_manager()); + olive::ExportVideoTab tab(oak_color_manager(project.color_manager())); tab.maintain_aspect_checkbox()->setChecked(true); EXPECT_FALSE(tab.scaling_method_combobox()->isEnabled()); @@ -313,7 +315,7 @@ TEST(DialogExportSavePreset, AcceptWritesPresetFile) { StandardPathsTestModeGuard test_mode; - olive::EncodingParams params; + OakEngineEncodingParams *params = oakengine_encoding_params_create(); olive::ExportSavePresetDialog dialog(params); diff --git a/tests/gtest/dialog_misc_test.cpp b/tests/gtest/dialog_misc_test.cpp index 5cc375dc3..ec898416b 100644 --- a/tests/gtest/dialog_misc_test.cpp +++ b/tests/gtest/dialog_misc_test.cpp @@ -23,6 +23,7 @@ #include "dialog/actionsearch/actionsearch.h" #include "dialog/autorecovery/autorecoverydialog.h" #include "dialog/color/colordialog.h" +#include "widget/manageddisplay/colorprocessorhandle.h" #include "dialog/configbase/configdialogbase.h" #include "dialog/diskcache/diskcachedialog.h" #include "dialog/preferences/keysequenceeditor.h" @@ -43,7 +44,7 @@ namespace void ensure_app_singletons() { if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } if (!olive::DiskManager::instance()) { olive::DiskManager::create_instance(); @@ -75,7 +76,7 @@ public: return validate_result; } - virtual void accept(olive::MultiUndoCommand *) override + virtual void accept(void *) override { ++accept_count; } @@ -502,10 +503,10 @@ TEST(DialogTask, WrapsAndOwnsTask) auto *task = new DummyTask(); QPointer task_guard(task); - auto *dialog = new olive::TaskDialog(task, QStringLiteral("Title")); + auto *dialog = new olive::TaskDialog( + reinterpret_cast(task), QStringLiteral("Title")); - EXPECT_EQ(dialog->get_task(), task); - EXPECT_EQ(task->parent(), dialog); + EXPECT_EQ(dialog->get_task(), reinterpret_cast(task)); // The dialog takes ownership of the task delete dialog; @@ -520,7 +521,7 @@ TEST(DialogColor, SelectedColorRoundTrips) olive::ColorManager::set_up_default_config(); olive::Project project; - olive::ColorDialog dialog(project.color_manager(), + olive::ColorDialog dialog(oak_color_manager(project.color_manager()), olive::Color(1.0f, 0.0f, 0.0f, 1.0f)); olive::ManagedColor selected = dialog.get_selected_color(); diff --git a/tests/gtest/footage_probe_test.cpp b/tests/gtest/footage_probe_test.cpp index db7a864d9..3ea694b5c 100644 --- a/tests/gtest/footage_probe_test.cpp +++ b/tests/gtest/footage_probe_test.cpp @@ -119,7 +119,7 @@ protected: { if (!olive::Core::instance()) { // Leaked intentionally: Core is process-wide (matches footage_test) - new olive::Core(olive::Core::CoreParams()); + new olive::Core(); } // Footage::Value() resolves Project::cache_path(), which goes through diff --git a/tests/gtest/footage_test.cpp b/tests/gtest/footage_test.cpp index 49cdfcd81..676ff35ba 100644 --- a/tests/gtest/footage_test.cpp +++ b/tests/gtest/footage_test.cpp @@ -343,7 +343,7 @@ protected: if (!olive::Core::instance()) { // Leaked intentionally: Core is process-wide and DiskManager // touches it (matches render_diskcache_test). - new olive::Core(olive::Core::CoreParams()); + new olive::Core(); } // Footage::Value() resolves Project::cache_path(), which goes through @@ -439,7 +439,10 @@ TEST_F(FootageTest, DataRolesForInvalidFootage) EXPECT_EQ(footage.data(olive::Node::tooltip).toString(), QStringLiteral("Invalid")); - EXPECT_TRUE(footage.data(olive::Node::icon).canConvert()); + // B1: engine icon sites return icon name strings (mapped to QIcon in the + // app layer via icon::from_name); invalid footage gets "error". + EXPECT_EQ(footage.data(olive::Node::icon).toString(), + QStringLiteral("error")); // With no existing file behind the footage, the time roles fall through // to the base class and stay invalid @@ -477,14 +480,8 @@ TEST_F(FootageTest, TooltipDescribesEnabledStreams) TEST_F(FootageTest, IconReflectsPrioritizedStreamType) { - // The icon globals must be loaded for the returned icons to be - // distinguishable (null icons all share the same cache key) - olive::icon::load_all(QStringLiteral(":/style/olive-dark")); - ASSERT_FALSE(olive::icon::video.isNull()); - ASSERT_FALSE(olive::icon::audio.isNull()); - ASSERT_FALSE(olive::icon::image.isNull()); - ASSERT_FALSE(olive::icon::subtitles.isNull()); - ASSERT_FALSE(olive::icon::error.isNull()); + // B1: engine icon sites return icon name strings ("video", "audio", + // "image", "subtitles", "error"); the QIcon mapping lives in the app layer. // Footage::data(ICON) only inspects streams once the footage has been // probed (total_stream_count_ is set by Reprobe), so each variant is @@ -507,8 +504,8 @@ TEST_F(FootageTest, IconReflectsPrioritizedStreamType) // Invalid footage gets the error icon TestableFootage invalid; - EXPECT_EQ(invalid.data(olive::Node::icon).value().cacheKey(), - olive::icon::error.cacheKey()); + EXPECT_EQ(invalid.data(olive::Node::icon).toString(), + QStringLiteral("error")); // Real video streams take priority over audio olive::FootageDescription video_audio(QStringLiteral("fakedecoder")); @@ -517,10 +514,8 @@ TEST_F(FootageTest, IconReflectsPrioritizedStreamType) video_audio.set_stream_count(2); TestableFootage *footage = probe(QStringLiteral("video-audio.mkv"), video_audio); ASSERT_NE(footage, nullptr); - const QIcon video_icon = footage->data(olive::Node::icon).value(); - EXPECT_EQ(video_icon.cacheKey(), olive::icon::video.cacheKey()); - EXPECT_NE(video_icon.cacheKey(), olive::icon::audio.cacheKey()); - EXPECT_NE(video_icon.cacheKey(), olive::icon::error.cacheKey()); + EXPECT_EQ(footage->data(olive::Node::icon).toString(), + QStringLiteral("video")); // Audio still takes priority over a still image stream olive::VideoParams still_stream = make_video_stream(0); @@ -532,10 +527,8 @@ TEST_F(FootageTest, IconReflectsPrioritizedStreamType) TestableFootage *still_and_audio = probe(QStringLiteral("still-audio.mkv"), still_audio); ASSERT_NE(still_and_audio, nullptr); - const QIcon still_audio_icon = - still_and_audio->data(olive::Node::icon).value(); - EXPECT_EQ(still_audio_icon.cacheKey(), olive::icon::audio.cacheKey()); - EXPECT_NE(still_audio_icon.cacheKey(), olive::icon::image.cacheKey()); + EXPECT_EQ(still_and_audio->data(olive::Node::icon).toString(), + QStringLiteral("audio")); // A still image without audio hits the image branch olive::FootageDescription stills(QStringLiteral("fakedecoder")); @@ -543,8 +536,8 @@ TEST_F(FootageTest, IconReflectsPrioritizedStreamType) stills.set_stream_count(1); TestableFootage *still_only = probe(QStringLiteral("still.mkv"), stills); ASSERT_NE(still_only, nullptr); - EXPECT_EQ(still_only->data(olive::Node::icon).value().cacheKey(), - olive::icon::image.cacheKey()); + EXPECT_EQ(still_only->data(olive::Node::icon).toString(), + QStringLiteral("image")); // Audio-only footage olive::FootageDescription audio(QStringLiteral("fakedecoder")); @@ -552,8 +545,8 @@ TEST_F(FootageTest, IconReflectsPrioritizedStreamType) audio.set_stream_count(1); TestableFootage *audio_only = probe(QStringLiteral("audio.mkv"), audio); ASSERT_NE(audio_only, nullptr); - EXPECT_EQ(audio_only->data(olive::Node::icon).value().cacheKey(), - olive::icon::audio.cacheKey()); + EXPECT_EQ(audio_only->data(olive::Node::icon).toString(), + QStringLiteral("audio")); // Subtitle-only footage olive::FootageDescription subs(QStringLiteral("fakedecoder")); @@ -561,8 +554,8 @@ TEST_F(FootageTest, IconReflectsPrioritizedStreamType) subs.set_stream_count(1); TestableFootage *subs_only = probe(QStringLiteral("subs.mkv"), subs); ASSERT_NE(subs_only, nullptr); - EXPECT_EQ(subs_only->data(olive::Node::icon).value().cacheKey(), - olive::icon::subtitles.cacheKey()); + EXPECT_EQ(subs_only->data(olive::Node::icon).toString(), + QStringLiteral("subtitles")); } TEST_F(FootageTest, ProxyChangesMarkProjectModifiedAndEmitSignal) diff --git a/tests/gtest/mainwindow_test.cpp b/tests/gtest/mainwindow_test.cpp index 40c709695..fd94a9e1e 100644 --- a/tests/gtest/mainwindow_test.cpp +++ b/tests/gtest/mainwindow_test.cpp @@ -21,10 +21,12 @@ #include "render/rendermanager.h" #include "task/task.h" #include "task/taskmanager.h" +#include "oakengine/task.h" +#include "engineeventbridge.h" #include "widget/menu/menushared.h" #include "window/mainwindow/mainstatusbar.h" #include "window/mainwindow/mainwindow.h" -#include "node/project/serializer/mainwindowlayoutinfo.h" +#include "node/project/serializer/serializedlayoutinfo.h" using namespace olive; @@ -47,7 +49,7 @@ protected: } // namespace -TEST(MainWindowLayoutInfo, AccessorsStoreAndRetrieve) +TEST(SerializedLayoutInfo, AccessorsStoreAndRetrieve) { Project project; project.initialize(); @@ -58,48 +60,49 @@ TEST(MainWindowLayoutInfo, AccessorsStoreAndRetrieve) auto *viewer = new ViewerOutput(); viewer->setParent(&project); - MainWindowLayoutInfo info; - EXPECT_TRUE(info.open_folders().empty()); - EXPECT_TRUE(info.open_sequences().empty()); - EXPECT_TRUE(info.open_viewers().empty()); - EXPECT_TRUE(info.panel_data().empty()); - EXPECT_TRUE(info.state().isEmpty()); + SerializedLayoutInfo info; + EXPECT_TRUE(info.open_folders.empty()); + EXPECT_TRUE(info.open_sequences.empty()); + EXPECT_TRUE(info.open_viewers.empty()); + EXPECT_TRUE(info.panel_data.empty()); + EXPECT_TRUE(info.state.isEmpty()); - info.add_folder(folder); - info.add_sequence(sequence); - info.add_viewer(viewer); + info.open_folders.push_back(folder); + info.open_sequences.push_back(sequence); + info.open_viewers.push_back(viewer); - ASSERT_EQ(info.open_folders().size(), 1); - EXPECT_EQ(info.open_folders().front(), folder); - ASSERT_EQ(info.open_sequences().size(), 1); - EXPECT_EQ(info.open_sequences().front(), sequence); - ASSERT_EQ(info.open_viewers().size(), 1); - EXPECT_EQ(info.open_viewers().front(), viewer); + ASSERT_EQ(info.open_folders.size(), 1); + EXPECT_EQ(info.open_folders.front(), folder); + ASSERT_EQ(info.open_sequences.size(), 1); + EXPECT_EQ(info.open_sequences.front(), sequence); + ASSERT_EQ(info.open_viewers.size(), 1); + EXPECT_EQ(info.open_viewers.front(), viewer); PanelWidget::Info data; data[QStringLiteral("key")] = QStringLiteral("value"); - info.set_panel_data(QStringLiteral("panel_a"), data); - ASSERT_EQ(info.panel_data().size(), 1); - EXPECT_EQ(info.panel_data() + info.panel_data[QStringLiteral("panel_a")] = data; + ASSERT_EQ(info.panel_data.size(), 1); + EXPECT_EQ(info.panel_data .at(QStringLiteral("panel_a")) .at(QStringLiteral("key")), QStringLiteral("value")); - // move_panel_data renames the entry - info.move_panel_data(QStringLiteral("panel_a"), - QStringLiteral("panel_b")); - EXPECT_EQ(info.panel_data().count(QStringLiteral("panel_a")), 0); - ASSERT_EQ(info.panel_data().count(QStringLiteral("panel_b")), 1); - EXPECT_EQ(info.panel_data() + // renaming the entry moves the data + info.panel_data[QStringLiteral("panel_b")] = + info.panel_data.at(QStringLiteral("panel_a")); + info.panel_data.erase(QStringLiteral("panel_a")); + EXPECT_EQ(info.panel_data.count(QStringLiteral("panel_a")), 0); + ASSERT_EQ(info.panel_data.count(QStringLiteral("panel_b")), 1); + EXPECT_EQ(info.panel_data .at(QStringLiteral("panel_b")) .at(QStringLiteral("key")), QStringLiteral("value")); - info.set_state(QByteArray("layout-state")); - EXPECT_EQ(info.state(), QByteArray("layout-state")); + info.state = QByteArray("layout-state"); + EXPECT_EQ(info.state, QByteArray("layout-state")); } -TEST(MainWindowLayoutInfo, XmlRoundTripPreservesEverything) +TEST(SerializedLayoutInfo, XmlRoundTripPreservesEverything) { Project project; project.initialize(); @@ -110,14 +113,14 @@ TEST(MainWindowLayoutInfo, XmlRoundTripPreservesEverything) auto *viewer = new ViewerOutput(); viewer->setParent(&project); - MainWindowLayoutInfo info; - info.add_folder(folder); - info.add_sequence(sequence); - info.add_viewer(viewer); + SerializedLayoutInfo info; + info.open_folders.push_back(folder); + info.open_sequences.push_back(sequence); + info.open_viewers.push_back(viewer); PanelWidget::Info data; data[QStringLiteral("splitter")] = QStringLiteral("AAA="); - info.set_panel_data(QStringLiteral("TimelinePanel"), data); - info.set_state(QByteArray("binary\x01\x02state", 12)); + info.panel_data[QStringLiteral("TimelinePanel")] = data; + info.state = QByteArray("binary\x01\x02state", 12); QString xml; QXmlStreamWriter writer(&xml); @@ -136,32 +139,32 @@ TEST(MainWindowLayoutInfo, XmlRoundTripPreservesEverything) ASSERT_TRUE(reader.readNextStartElement()); ASSERT_EQ(reader.name(), QStringLiteral("layout")); - MainWindowLayoutInfo loaded = MainWindowLayoutInfo::from_xml(&reader, node_map); + SerializedLayoutInfo loaded = SerializedLayoutInfo::from_xml(&reader, node_map); - ASSERT_EQ(loaded.open_folders().size(), 1); - EXPECT_EQ(loaded.open_folders().front(), folder); - ASSERT_EQ(loaded.open_sequences().size(), 1); - EXPECT_EQ(loaded.open_sequences().front(), sequence); + ASSERT_EQ(loaded.open_folders.size(), 1); + EXPECT_EQ(loaded.open_folders.front(), folder); + ASSERT_EQ(loaded.open_sequences.size(), 1); + EXPECT_EQ(loaded.open_sequences.front(), sequence); // Open viewers must survive the round trip too - ASSERT_EQ(loaded.open_viewers().size(), 1); - EXPECT_EQ(loaded.open_viewers().front(), viewer); + ASSERT_EQ(loaded.open_viewers.size(), 1); + EXPECT_EQ(loaded.open_viewers.front(), viewer); - EXPECT_EQ(loaded.state(), info.state()); + EXPECT_EQ(loaded.state, info.state); - ASSERT_EQ(loaded.panel_data().count(QStringLiteral("TimelinePanel")), 1); - EXPECT_EQ(loaded.panel_data() + ASSERT_EQ(loaded.panel_data.count(QStringLiteral("TimelinePanel")), 1); + EXPECT_EQ(loaded.panel_data .at(QStringLiteral("TimelinePanel")) .at(QStringLiteral("splitter")), QStringLiteral("AAA=")); // No unknown nodes leak into the viewers list: it must contain exactly the // viewer that was added, not a duplicate of the sequences list - EXPECT_NE(loaded.open_viewers().front(), + EXPECT_NE(loaded.open_viewers.front(), static_cast(sequence)); } -TEST(MainWindowLayoutInfo, FromXmlSkipsUnknownElementsAndNodes) +TEST(SerializedLayoutInfo, FromXmlSkipsUnknownElementsAndNodes) { const QString xml = QStringLiteral( "" @@ -177,17 +180,17 @@ TEST(MainWindowLayoutInfo, FromXmlSkipsUnknownElementsAndNodes) ASSERT_TRUE(reader.readNextStartElement()); ASSERT_EQ(reader.name(), QStringLiteral("layout")); - MainWindowLayoutInfo info = MainWindowLayoutInfo::from_xml(&reader, {}); + SerializedLayoutInfo info = SerializedLayoutInfo::from_xml(&reader, {}); // Unknown pointers resolve to null but are still listed - ASSERT_EQ(info.open_folders().size(), 1); - EXPECT_EQ(info.open_folders().front(), nullptr); - ASSERT_EQ(info.open_sequences().size(), 1); - EXPECT_EQ(info.open_sequences().front(), nullptr); - ASSERT_EQ(info.open_viewers().size(), 1); - EXPECT_EQ(info.open_viewers().front(), nullptr); - EXPECT_EQ(info.state(), QByteArray("ABC")); - EXPECT_TRUE(info.panel_data().empty()); + ASSERT_EQ(info.open_folders.size(), 1); + EXPECT_EQ(info.open_folders.front(), nullptr); + ASSERT_EQ(info.open_sequences.size(), 1); + EXPECT_EQ(info.open_sequences.front(), nullptr); + ASSERT_EQ(info.open_viewers.size(), 1); + EXPECT_EQ(info.open_viewers.front(), nullptr); + EXPECT_EQ(info.state, QByteArray("ABC")); + EXPECT_TRUE(info.panel_data.empty()); } TEST(MainWindowStatusBar, ConstructionDefaults) @@ -202,29 +205,36 @@ TEST(MainWindowStatusBar, ConstructionDefaults) TEST(MainWindowStatusBar, ReflectsTaskManagerState) { - TaskManager manager; + // The status bar now uses the global TaskManager singleton via the facade. + if (!TaskManager::instance()) { + TaskManager::create_instance(); + } + EngineEventBridge bridge; MainStatusBar bar; bar.show(); auto *progress = bar.findChild(); ASSERT_NE(progress, nullptr); - bar.connect_task_manager(&manager); + bar.connect_task_manager(&bridge); auto *task = new DummyTask(); - manager.add_task(task); + oakengine_task_manager_add( + reinterpret_cast(task)); // One running task shows its title and the progress bar EXPECT_EQ(bar.currentMessage(), QStringLiteral("Status Test Task")); EXPECT_TRUE(progress->isVisible()); - // Progress signals are forwarded to the bar + // Progress signals are forwarded to the bar through the facade event bridge emit task->progress_changed(0.5); - EXPECT_EQ(progress->value(), 50); + QTRY_COMPARE_WITH_TIMEOUT(progress->value(), 50, 1000); // When the task list empties, the bar hides and the message clears - manager.cancel_task_and_wait(task); - QTRY_COMPARE_WITH_TIMEOUT(manager.get_task_count(), 0, 2000); + oakengine_task_manager_cancel( + reinterpret_cast(task)); + // Wait for the deferred delete on the task (facade cancel removes it) + QTRY_COMPARE_WITH_TIMEOUT(oakengine_task_manager_count(), 0, 2000); EXPECT_TRUE(bar.currentMessage().isEmpty()); EXPECT_FALSE(progress->isVisible()); EXPECT_EQ(progress->value(), 0); @@ -300,7 +310,7 @@ TEST(MainWindow, ConstructsOffscreenWithPanelsAndMenus) AudioManager::create_instance(); } if (!Core::instance()) { - new Core(Core::CoreParams()); // intentionally leaked + new Core(); // intentionally leaked } KDDockWidgets::initFrontend(KDDockWidgets::FrontendType::QtWidgets); diff --git a/tests/gtest/node_core_test.cpp b/tests/gtest/node_core_test.cpp index 117a26751..f419511db 100644 --- a/tests/gtest/node_core_test.cpp +++ b/tests/gtest/node_core_test.cpp @@ -17,6 +17,7 @@ #include "node/node.h" #include "node/project.h" #include "node/project/folder/folder.h" +#include "oakengine/node.h" namespace { @@ -774,9 +775,9 @@ TEST_F(NodeCoreTest, KeyframeAddAndRemovalEmitSignals) int added = 0; int removed = 0; QObject::connect(node, &olive::Node::keyframe_added, - [&added](olive::NodeKeyframe *) { ++added; }); + [&added](OakEngineKeyframe *) { ++added; }); QObject::connect(node, &olive::Node::keyframe_removed, - [&removed](olive::NodeKeyframe *) { ++removed; }); + [&removed](OakEngineKeyframe *) { ++removed; }); QVector changed_ranges; QObject::connect(node, &olive::Node::value_changed, [&changed_ranges](const olive::NodeInput &, @@ -854,9 +855,9 @@ TEST_F(NodeCoreTest, KeyframeTimeChangeResortsTrackAndEmits) olive::NodeKeyframe *last_changed = nullptr; QObject::connect(node, &olive::Node::keyframe_time_changed, [&time_changed, - &last_changed](olive::NodeKeyframe *key) { + &last_changed](OakEngineKeyframe *key) { ++time_changed; - last_changed = key; + last_changed = reinterpret_cast(key); }); // Moving the first keyframe past the second resorts the track diff --git a/tests/gtest/node_polygon_folder_test.cpp b/tests/gtest/node_polygon_folder_test.cpp index 7f2052091..730a3834c 100644 --- a/tests/gtest/node_polygon_folder_test.cpp +++ b/tests/gtest/node_polygon_folder_test.cpp @@ -113,7 +113,7 @@ olive::TexturePtr get_output_texture(const olive::NodeValueTable &table) void ensure_app_singletons() { if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } if (!olive::DiskManager::instance()) { olive::DiskManager::create_instance(); diff --git a/tests/gtest/node_save_load_test.cpp b/tests/gtest/node_save_load_test.cpp index c500c4526..3fbdaee8c 100644 --- a/tests/gtest/node_save_load_test.cpp +++ b/tests/gtest/node_save_load_test.cpp @@ -147,7 +147,7 @@ protected: // singleton, which itself touches Core (same pattern as // project_factory_test) if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } if (!olive::DiskManager::instance()) { olive::DiskManager::create_instance(); diff --git a/tests/gtest/panel_test.cpp b/tests/gtest/panel_test.cpp index 2a869e08d..b5d7ba6e1 100644 --- a/tests/gtest/panel_test.cpp +++ b/tests/gtest/panel_test.cpp @@ -63,7 +63,7 @@ public: ColorManager::set_up_default_config(); if (!Core::instance()) { - new Core(Core::CoreParams()); // intentionally leaked + new Core(); // intentionally leaked } KDDockWidgets::initFrontend(KDDockWidgets::FrontendType::QtWidgets); @@ -401,7 +401,7 @@ TEST_F(PanelTest, CurvePanelSetNodes) // A single node appears as one top-level item listing its keyframable // inputs (MathNode has three: the base "enabled" input and parameters // A and B) - panel.set_node(math); + panel.set_node(reinterpret_cast(math)); ASSERT_EQ(tree->topLevelItemCount(), 1); EXPECT_EQ(tree->topLevelItem(0)->text(0), math->name()); EXPECT_EQ(tree->topLevelItem(0)->childCount(), 3); @@ -447,12 +447,12 @@ TEST_F(PanelTest, ParamPanelForwardsViewSignals) ParamPanel panel; QSignalSpy focused_spy(&panel, &ParamPanel::focused_node_changed); - emit panel.get_param_view()->focused_node_changed(math); + emit panel.get_param_view()->focused_node_changed(reinterpret_cast(math)); ASSERT_EQ(focused_spy.count(), 1); - EXPECT_EQ(focused_spy.first().first().value(), math); + EXPECT_EQ(focused_spy.first().first().value(), reinterpret_cast(math)); QSignalSpy selected_spy(&panel, &ParamPanel::selected_nodes_changed); - emit panel.get_param_view()->selected_nodes_changed({ { math, nullptr } }); + emit panel.get_param_view()->selected_nodes_changed({ { reinterpret_cast(math), nullptr } }); EXPECT_EQ(selected_spy.count(), 1); QSignalSpy text_spy(&panel, &ParamPanel::request_viewer_to_start_editing_text); @@ -620,7 +620,7 @@ TEST_F(PanelTest, FootageViewerPanelConstruction) panel.connect_viewer_node(viewer); ASSERT_EQ(panel.get_selected_footage().size(), 1); - EXPECT_EQ(panel.get_selected_footage().first(), viewer); + EXPECT_EQ(panel.get_selected_footage().first(), reinterpret_cast(viewer)); panel.disconnect_viewer_node(); EXPECT_TRUE(panel.get_selected_footage().isEmpty()); @@ -656,15 +656,15 @@ TEST_F(PanelTest, NodePanelForwardsViewSignals) panel.set_contexts({ project.root() }); QSignalSpy selected_spy(&panel, &NodePanel::nodes_selected); - emit panel.get_node_widget()->view()->nodes_selected({ math }); + emit panel.get_node_widget()->view()->nodes_selected({ reinterpret_cast(math) }); ASSERT_EQ(selected_spy.count(), 1); QSignalSpy deselected_spy(&panel, &NodePanel::nodes_deselected); - emit panel.get_node_widget()->view()->nodes_deselected({ math }); + emit panel.get_node_widget()->view()->nodes_deselected({ reinterpret_cast(math) }); EXPECT_EQ(deselected_spy.count(), 1); QSignalSpy selection_spy(&panel, &NodePanel::node_selection_changed); - emit panel.get_node_widget()->view()->node_selection_changed({ math }); + emit panel.get_node_widget()->view()->node_selection_changed({ reinterpret_cast(math) }); EXPECT_EQ(selection_spy.count(), 1); } diff --git a/tests/gtest/project_factory_test.cpp b/tests/gtest/project_factory_test.cpp index 43dcaaf41..e2607dfb3 100644 --- a/tests/gtest/project_factory_test.cpp +++ b/tests/gtest/project_factory_test.cpp @@ -54,7 +54,7 @@ void collect_leaf_actions(QMenu *menu, QList *leaves) void ensure_app_singletons() { if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } if (!olive::DiskManager::instance()) { olive::DiskManager::create_instance(); diff --git a/tests/gtest/render_diskcache_test.cpp b/tests/gtest/render_diskcache_test.cpp index 1f27a8b6d..26a974ae7 100644 --- a/tests/gtest/render_diskcache_test.cpp +++ b/tests/gtest/render_diskcache_test.cpp @@ -71,7 +71,7 @@ protected: // Leaked intentionally: Core is process-wide and DiskCacheFolder // eviction calls Core::instance()->WarnCacheFull() (matches // viewer_display_repro_test). - new olive::Core(olive::Core::CoreParams()); + new olive::Core(); } olive::DiskManager::create_instance(); diff --git a/tests/gtest/render_processor_test.cpp b/tests/gtest/render_processor_test.cpp index c516158f5..7c73d8a17 100644 --- a/tests/gtest/render_processor_test.cpp +++ b/tests/gtest/render_processor_test.cpp @@ -41,7 +41,7 @@ #include "node/globals.h" #include "node/project.h" #include "render/job/acceleratedjob.h" -#include "render/managedcolor.h" +#include "widget/manageddisplay/colorprocessorhandle.h" #include "render/renderjobtracker.h" #include "render/rendermanager.h" #include "render/renderprocessor.h" diff --git a/tests/gtest/render_projectcopier_test.cpp b/tests/gtest/render_projectcopier_test.cpp index 184eef414..0b24b4c50 100644 --- a/tests/gtest/render_projectcopier_test.cpp +++ b/tests/gtest/render_projectcopier_test.cpp @@ -75,7 +75,7 @@ protected: if (!olive::Core::instance()) { // Leaked intentionally: Core is process-wide (matches // render_diskcache_test) - new olive::Core(olive::Core::CoreParams()); + new olive::Core(); } olive::DiskManager::create_instance(); diff --git a/tests/gtest/render_tail_test.cpp b/tests/gtest/render_tail_test.cpp index 15eb280bf..5953cad0e 100644 --- a/tests/gtest/render_tail_test.cpp +++ b/tests/gtest/render_tail_test.cpp @@ -466,7 +466,7 @@ protected: if (!olive::Core::instance()) { // Leaked intentionally: matches render_diskcache_test, Core is // process-wide and eviction paths call Core::WarnCacheFull(). - new olive::Core(olive::Core::CoreParams()); + new olive::Core(); } olive::DiskManager::create_instance(); @@ -658,7 +658,7 @@ protected: } if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } olive::DiskManager::create_instance(); diff --git a/tests/gtest/task_cache_test.cpp b/tests/gtest/task_cache_test.cpp index aba698c57..899dce9df 100644 --- a/tests/gtest/task_cache_test.cpp +++ b/tests/gtest/task_cache_test.cpp @@ -90,7 +90,7 @@ protected: { if (!olive::Core::instance()) { // Leaked intentionally: Core is process-wide (matches footage_probe_test) - new olive::Core(olive::Core::CoreParams()); + new olive::Core(); } olive::ColorManager::set_up_default_config(); diff --git a/tests/gtest/task_project_test.cpp b/tests/gtest/task_project_test.cpp index 11fe04ccb..fa8944523 100644 --- a/tests/gtest/task_project_test.cpp +++ b/tests/gtest/task_project_test.cpp @@ -40,7 +40,7 @@ protected: { if (!olive::Core::instance()) { // Leaked intentionally: Core is process-wide (matches footage_probe_test) - new olive::Core(olive::Core::CoreParams()); + new olive::Core(); } created_disk_manager_ = (olive::DiskManager::instance() == nullptr); @@ -109,9 +109,10 @@ TEST_F(TaskProjectImportTest, ImportOfUnprobeableFileCollectsInvalidList) EXPECT_DOUBLE_EQ(last_progress, 1.0); // The undo command exists but contains no children since nothing was added - ASSERT_NE(task.get_command(), nullptr); - EXPECT_EQ(task.get_command()->child_count(), 0); - delete task.get_command(); + olive::MultiUndoCommand *cmd = task.take_command(); + ASSERT_NE(cmd, nullptr); + EXPECT_EQ(cmd->child_count(), 0); + delete cmd; } TEST_F(TaskProjectImportTest, ImportOfImageFileAddsFootageThroughUndoCommand) @@ -135,18 +136,19 @@ TEST_F(TaskProjectImportTest, ImportOfImageFileAddsFootageThroughUndoCommand) // Nothing is in the folder until the command is redone EXPECT_TRUE(project_->root()->children().isEmpty()); - ASSERT_NE(task.get_command(), nullptr); - task.get_command()->redo_now(); + olive::MultiUndoCommand *cmd = task.take_command(); + ASSERT_NE(cmd, nullptr); + cmd->redo_now(); ASSERT_EQ(project_->root()->children().size(), 1); EXPECT_EQ(project_->root()->children().first(), static_cast(footage)); EXPECT_TRUE(project_->nodes().contains(footage)); - task.get_command()->undo_now(); + cmd->undo_now(); EXPECT_TRUE(project_->root()->children().isEmpty()); - delete task.get_command(); + delete cmd; } TEST_F(TaskProjectImportTest, ImportOfDirectoryCreatesFolderHierarchy) @@ -171,8 +173,9 @@ TEST_F(TaskProjectImportTest, ImportOfDirectoryCreatesFolderHierarchy) EXPECT_FALSE(task.has_invalid_files()); EXPECT_EQ(task.get_imported_footage().size(), 2); - ASSERT_NE(task.get_command(), nullptr); - task.get_command()->redo_now(); + olive::MultiUndoCommand *cmd = task.take_command(); + ASSERT_NE(cmd, nullptr); + cmd->redo_now(); // Importing a directory creates a folder named after it under the target const QVector &root_children = project_->root()->children(); @@ -191,10 +194,10 @@ TEST_F(TaskProjectImportTest, ImportOfDirectoryCreatesFolderHierarchy) ASSERT_EQ(sub_folders.size(), 1); EXPECT_EQ(sub_folders.first()->get_label(), QStringLiteral("sub")); - task.get_command()->undo_now(); + cmd->undo_now(); EXPECT_TRUE(project_->root()->children().isEmpty()); - delete task.get_command(); + delete cmd; } TEST_F(TaskProjectImportTest, CancelledBeforeRunReturnsFalseAndDropsCommand) @@ -206,7 +209,7 @@ TEST_F(TaskProjectImportTest, CancelledBeforeRunReturnsFalseAndDropsCommand) task.Cancel(); EXPECT_FALSE(task.start()); - EXPECT_EQ(task.get_command(), nullptr); + EXPECT_EQ(task.take_command(), nullptr); EXPECT_TRUE(task.get_imported_footage().isEmpty()); EXPECT_FALSE(task.has_invalid_files()); EXPECT_TRUE(project_->root()->children().isEmpty()); diff --git a/tests/gtest/viewer_display_repro_test.cpp b/tests/gtest/viewer_display_repro_test.cpp index 2204a2d16..7550a93f6 100644 --- a/tests/gtest/viewer_display_repro_test.cpp +++ b/tests/gtest/viewer_display_repro_test.cpp @@ -152,7 +152,7 @@ protected: } if (!Core::instance()) { - new Core(Core::CoreParams()); + new Core(); } AudioManager::create_instance(); } diff --git a/tests/gtest/widget_curve_keyframe_test.cpp b/tests/gtest/widget_curve_keyframe_test.cpp index 967109e41..b7014b917 100644 --- a/tests/gtest/widget_curve_keyframe_test.cpp +++ b/tests/gtest/widget_curve_keyframe_test.cpp @@ -28,7 +28,7 @@ namespace void ensure_app_singletons() { if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } if (!olive::DiskManager::instance()) { olive::DiskManager::create_instance(); diff --git a/tests/gtest/widget_misc_test.cpp b/tests/gtest/widget_misc_test.cpp index c580dc847..24d0df672 100644 --- a/tests/gtest/widget_misc_test.cpp +++ b/tests/gtest/widget_misc_test.cpp @@ -35,6 +35,7 @@ #include "widget/filefield/filefield.h" #include "widget/focusablelineedit/focusablelineedit.h" #include "widget/handmovableview/handmovableview.h" +#include "widget/manageddisplay/colorprocessorhandle.h" #include "widget/menu/menu.h" #include "widget/nodevaluetree/nodevaluetree.h" #include "widget/path/pathwidget.h" @@ -52,7 +53,7 @@ namespace void ensure_core() { if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } } @@ -641,7 +642,7 @@ TEST(WidgetColorButton, SetColorRoundTrips) olive::ColorManager::set_up_default_config(); olive::Project project; - olive::ColorButton btn(project.color_manager()); + olive::ColorButton btn(oak_color_manager(project.color_manager())); EXPECT_FLOAT_EQ(btn.get_color().red(), 1.0f); EXPECT_FLOAT_EQ(btn.get_color().green(), 1.0f); EXPECT_FLOAT_EQ(btn.get_color().blue(), 1.0f); @@ -716,7 +717,7 @@ TEST(WidgetColorSwatchChooser, ClickingSwatchEmitsItsColor) olive::ColorManager::set_up_default_config(); olive::Project project; - olive::ColorSwatchChooser chooser(project.color_manager()); + olive::ColorSwatchChooser chooser(oak_color_manager(project.color_manager())); const auto buttons = chooser.findChildren(); EXPECT_EQ(buttons.size(), 32); @@ -746,7 +747,7 @@ TEST(WidgetColorSpaceChooser, InputRoundTripsAndEmits) ASSERT_GE(spaces.size(), 2); // Input-only mode, as used by the export dialog - olive::ColorSpaceChooser chooser(project.color_manager(), true, false); + olive::ColorSpaceChooser chooser(oak_color_manager(project.color_manager()), true, false); EXPECT_FALSE(chooser.input().isEmpty()); QSignalSpy spy(&chooser, @@ -773,7 +774,7 @@ TEST(WidgetColorSpaceChooser, FullModePopulatesDisplayFields) olive::ColorManager::set_up_default_config(); olive::Project project; - olive::ColorSpaceChooser chooser(project.color_manager()); + olive::ColorSpaceChooser chooser(oak_color_manager(project.color_manager())); EXPECT_FALSE(chooser.input().isEmpty()); EXPECT_FALSE(chooser.output().display().isEmpty()); EXPECT_FALSE(chooser.output().view().isEmpty()); diff --git a/tests/gtest/widget_panels_model_test.cpp b/tests/gtest/widget_panels_model_test.cpp index a6210e0f8..036ccd22d 100644 --- a/tests/gtest/widget_panels_model_test.cpp +++ b/tests/gtest/widget_panels_model_test.cpp @@ -45,7 +45,7 @@ namespace void ensure_app_singletons() { if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } if (!olive::DiskManager::instance()) { olive::DiskManager::create_instance(); @@ -253,8 +253,8 @@ TEST_F(WidgetPanelsTest, NodeTreeCheckboxesToggleEnableStateAndEmit) int node_emissions = 0; QObject::connect(&view, &NodeTreeView::node_enable_changed, [&node_signal_node, &node_signal_enabled, - &node_emissions](Node *n, bool e) { - node_signal_node = n; + &node_emissions](OakEngineNode *n, bool e) { + node_signal_node = reinterpret_cast(n); node_signal_enabled = e; ++node_emissions; }); @@ -431,7 +431,7 @@ TEST(TaskView, TaskLifecycleUpdatesItems) TaskView view(nullptr); DummyTask task; - view.add_task(&task); + view.add_task(reinterpret_cast(&task)); auto *item = view.findChild(); ASSERT_NE(item, nullptr); @@ -453,16 +453,16 @@ TEST(TaskView, TaskLifecycleUpdatesItems) EXPECT_EQ(bar->value(), 50); // The cancel button relays the task through TaskCancelled - Task *cancelled = nullptr; + OakEngineTask *cancelled = nullptr; QObject::connect(&view, &TaskView::task_cancelled, - [&cancelled](Task *t) { cancelled = t; }); + [&cancelled](OakEngineTask *t) { cancelled = t; }); auto *cancel_button = item->findChild(); ASSERT_NE(cancel_button, nullptr); cancel_button->click(); - EXPECT_EQ(cancelled, &task); + EXPECT_EQ(cancelled, reinterpret_cast(&task)); // Failure swaps in the error label - view.task_failed(&task); + view.task_failed(reinterpret_cast(&task)); bool found_error = false; foreach (QLabel *label, item->findChildren()) { if (label->text().contains(QStringLiteral("boom"))) { @@ -473,7 +473,7 @@ TEST(TaskView, TaskLifecycleUpdatesItems) EXPECT_TRUE(found_error); // Removal deletes the item once deferred deletions are processed - view.remove_task(&task); + view.remove_task(reinterpret_cast(&task)); QCoreApplication::sendPostedEvents(nullptr, QEvent::DeferredDelete); EXPECT_EQ(view.findChild(), nullptr); } diff --git a/tests/gtest/widget_projectexplorer_test.cpp b/tests/gtest/widget_projectexplorer_test.cpp index 82d58656b..beaa0e4c1 100644 --- a/tests/gtest/widget_projectexplorer_test.cpp +++ b/tests/gtest/widget_projectexplorer_test.cpp @@ -28,7 +28,7 @@ namespace void ensure_app_singletons() { if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } if (!olive::DiskManager::instance()) { olive::DiskManager::create_instance(); diff --git a/tests/gtest/widget_timeruler_playback_test.cpp b/tests/gtest/widget_timeruler_playback_test.cpp index d34bd45af..adb4c0e31 100644 --- a/tests/gtest/widget_timeruler_playback_test.cpp +++ b/tests/gtest/widget_timeruler_playback_test.cpp @@ -26,7 +26,7 @@ namespace void ensure_app_singletons() { if (!olive::Core::instance()) { - new olive::Core(olive::Core::CoreParams()); // intentionally leaked + new olive::Core(); // intentionally leaked } if (!olive::DiskManager::instance()) { olive::DiskManager::create_instance(); diff --git a/worker/CMakeLists.txt b/worker/CMakeLists.txt index a8f8d7ba2..c7ebef01a 100644 --- a/worker/CMakeLists.txt +++ b/worker/CMakeLists.txt @@ -17,36 +17,26 @@ # Render worker process (oak-render-worker). # # The worker is a headless render process spawned by the editor through -# RenderWorkerPool. It reuses the libolive-editor object library so it -# shares the exact same render/node/codec code as the editor. The link set -# is currently the full OLIVE_LIBRARIES for simplicity; trimming UI-only -# dependencies is a later-phase cleanup (render-process-isolation plan). +# RenderWorkerPool. It is a thin shell over liboakengine's pure C ABI +# (oakengine/worker.h): all render/node/codec logic lives inside the engine +# library, so the worker executable imports no engine C++ symbols and only +# sees the C headers under engine/include. add_executable(olive-render-worker workermain.cpp $ ) set_target_properties(olive-render-worker PROPERTIES OUTPUT_NAME "oak-render-worker") -if (APPLE) - target_sources(olive-render-worker PRIVATE workermain_mac.mm) - target_link_libraries(olive-render-worker PRIVATE "-framework Cocoa") -endif () - -# CMAKE_INCLUDE_CURRENT_DIR only covers the app/ scope, so add the app -# include roots explicitly now that the worker lives outside it +# $ links liboakengine without inheriting its usage +# requirements (the engine's C++ include roots); the worker is only allowed +# to see the public C API headers. +target_link_libraries(olive-render-worker PRIVATE $) target_include_directories(olive-render-worker PRIVATE - ${CMAKE_SOURCE_DIR}/app - ${CMAKE_BINARY_DIR}/app - ${CMAKE_SOURCE_DIR}/app/pluginSupport - ${OLIVE_INCLUDE_DIRS}) - -target_link_libraries(olive-render-worker PUBLIC OfxHost) -target_link_libraries(olive-render-worker PRIVATE ${OLIVE_LIBRARIES}) -target_compile_options(olive-render-worker PRIVATE ${OLIVE_COMPILE_OPTIONS}) -target_compile_definitions(olive-render-worker PRIVATE ${OLIVE_DEFINITIONS}) + ${CMAKE_SOURCE_DIR}/engine/include) +# The worker needs the dynamic render backend plugins at runtime; build them +# first so the worker is never run against stale backends. if (OAK_ENABLE_DYNAMIC_RENDER_BACKEND) - target_compile_definitions(olive-render-worker PRIVATE OAK_ENABLE_DYNAMIC_RENDER_BACKEND) add_dependencies(olive-render-worker oakgl) if (TARGET oakvulkan) add_dependencies(olive-render-worker oakvulkan) diff --git a/worker/workermain.cpp b/worker/workermain.cpp index fa1e74876..b76cc049a 100644 --- a/worker/workermain.cpp +++ b/worker/workermain.cpp @@ -18,736 +18,13 @@ ***/ -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#ifdef Q_OS_LINUX -#include -#include -#endif - -#include "common/qtutils.h" -#include "config/config.h" -#include "coreengine.h" -#include "node/factory.h" -#include "node/input/multicam/multicamnode.h" -#include "node/project/serializer/serializer.h" -#include "render/diskmanager.h" -#include "render/framemanager.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" -#include "render/colorprocessor.h" -#include "render/colortransform.h" - -#ifdef Q_OS_MACOS -void HideWorkerDockIcon(); -#endif - -namespace -{ - -#ifdef Q_OS_LINUX -void print_backtrace(int sig) -{ - void *array[50]; - size_t size = backtrace(array, 50); - fprintf(stderr, "worker: caught signal %d, backtrace:\n", sig); - backtrace_symbols_fd(array, size, STDERR_FILENO); - fflush(stderr); - _exit(128 + sig); -} -#endif - -constexpr int k_protocol_version = 1; -constexpr int k_default_width = 1920; -constexpr int k_default_height = 1080; -constexpr int k_default_frame_rate = 24; - -void install_surface_format() -{ - QSurfaceFormat format; - format.setVersion(3, 2); - format.setProfile(QSurfaceFormat::CoreProfile); - format.setDepthBufferSize(24); - QSurfaceFormat::setDefaultFormat(format); -} - -void log_error(const QString &message) -{ - const QByteArray line = QByteArray("worker: ") + message.toUtf8() + '\n'; - fwrite(line.constData(), 1, size_t(line.size()), stderr); - fflush(stderr); -} - -QJsonObject error_message(const QString &message, qint64 ticket_id = 0) -{ - QJsonObject o; - o["type"] = olive::ipc::msgtype::k_error; - 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::DiskManager::destroy_instance(); - olive::FrameManager::destroy_instance(); - olive::NodeFactory::destroy(); - } - - bool initialize_runtime() - { - // Create a minimal EngineCore instance so that code paths calling - // EngineCore::instance() (e.g. ViewerOutput::data for timecode display) - // do not dereference null. The worker has no UI, so the plain engine - // core is sufficient. The worker is short-lived; leaking this on exit - // is harmless. - if (!olive::EngineCore::instance()) { - new olive::EngineCore(olive::EngineCore::CoreParams()); - } - - olive::Config::load(); - olive::NodeFactory::initialize(); - olive::ColorManager::set_up_default_config(); - olive::FrameManager::create_instance(); - olive::DiskManager::create_instance(); - olive::ProjectSerializer::initialize(); - return true; - } - - bool send_startup_handshake() - { - olive::ipc::HandshakeMsg hs; - hs.protocol_version = k_protocol_version; - 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.to_json(); - QOpenGLContext *ctx = nullptr; -#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND - if (auto *dynamic_renderer = - dynamic_cast(renderer_)) { - ctx = dynamic_renderer->open_gl_context(); - } else -#endif - { - ctx = static_cast(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::k_handshake)) { - olive::ipc::HandshakeMsg hs; - if (!olive::ipc::HandshakeMsg::from_json(message, &hs)) { - return write( - error_message(QStringLiteral("invalid handshake message"))); - } - return attach_output_pool(hs); - } - - if (type == QLatin1String(olive::ipc::msgtype::k_load_graph)) { - olive::ipc::LoadGraphMsg load; - if (!olive::ipc::LoadGraphMsg::from_json(message, &load)) { - return write( - error_message(QStringLiteral("invalid load_graph message"))); - } - return load_graph(load.path); - } - - if (type == QLatin1String(olive::ipc::msgtype::k_render_frame)) { - olive::ipc::RenderFrameMsg render; - if (!olive::ipc::RenderFrameMsg::from_json(message, &render)) { - return write(error_message( - QStringLiteral("invalid render_frame message"))); - } - return render_frame(render); - } - - if (type == QLatin1String(olive::ipc::msgtype::k_cancel)) { - // Stage 5 wires cancellation into in-flight jobs. Stage 2 has only synchronous single-frame work. - return true; - } - - if (type == QLatin1String(olive::ipc::msgtype::k_shutdown)) { - shutdown_requested_ = true; - return true; - } - - return write( - error_message(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::write_message(out_, message); - out_->flush(); - return ok; - } - - bool attach_output_pool(const olive::ipc::HandshakeMsg &hs) - { - if (hs.protocol_version != k_protocol_version) { - return write( - error_message(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(error_message(QStringLiteral( - "handshake missing output shared-memory geometry"))); - } - - const size_t bytes = olive::ipc::FrameSlotPool::bytes_needed( - uint32_t(hs.output_slots), size_t(hs.slot_data_bytes)); - if (!output_region_.open(hs.shm_key, bytes, - olive::ipc::SharedMemoryRegion::k_attach)) { - return write(error_message( - QStringLiteral("failed to attach shared memory: %1") - .arg(output_region_.error()))); - } - - output_pool_ = olive::ipc::FrameSlotPool::attach(output_region_.data()); - if (!output_pool_->is_valid()) { - output_region_.close(); - output_pool_.reset(); - return write(error_message(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(error_message(QStringLiteral( - "handshake missing input shared-memory geometry"))); - } - - const size_t input_bytes = olive::ipc::FrameSlotPool::bytes_needed( - 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::k_attach)) { - return write(error_message( - QStringLiteral("failed to attach input shared memory: %1") - .arg(input_region_.error()))); - } - - input_pool_ = - olive::ipc::FrameSlotPool::attach(input_region_.data()); - if (!input_pool_->is_valid()) { - input_region_.close(); - input_pool_.reset(); - return write(error_message(QStringLiteral( - "input shared memory does not contain a frame slot pool"))); - } - } - - return true; - } - - bool load_graph(const QString &path) - { - { - QFileInfo fi(path); - if (!fi.exists()) { - log_error( - QStringLiteral("LoadGraph: graph file does not exist: %1") - .arg(path)); - return write(error_message( - QStringLiteral("graph file does not exist: %1").arg(path))); - } - if (fi.size() == 0) { - log_error(QStringLiteral("LoadGraph: graph file is empty: %1") - .arg(path)); - return write(error_message( - QStringLiteral("graph file is empty: %1").arg(path))); - } - log_error( - QStringLiteral("LoadGraph: loading %1 (%2 bytes, readable=%3)") - .arg(path) - .arg(fi.size()) - .arg(fi.isReadable())); - } - - auto loaded = std::make_unique(); - // Do not call Initialize() here: project serializers expect a blank - // project (root_ == nullptr) and will set root themselves. Calling - // Initialize() first triggers Q_ASSERT(!root_) in Project::Load. - - olive::ProjectSerializer::Result result = - olive::ProjectSerializer::load(loaded.get(), path, - olive::ProjectSerializer::k_project); - if (result != olive::ProjectSerializer::k_success) { - return write( - error_message(QStringLiteral("failed to load graph %1: %2") - .arg(path, result.get_details()))); - } - - project_ = std::move(loaded); - node_by_token_.clear(); - color_processor_cache_.clear(); - - const auto &data = result.get_load_data(); - 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 *find_node(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 render_frame(const olive::ipc::RenderFrameMsg &message) - { - if (!project_) { - return write(error_message( - QStringLiteral("render_frame received before load_graph"), - message.ticket_id)); - } - if (!output_pool_ || !output_pool_->is_valid()) { - return write(error_message( - QStringLiteral( - "render_frame received before output shm handshake"), - message.ticket_id)); - } - - olive::Node *node = find_node(message.node_uuid); - if (!node) { - return write( - error_message(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_->is_valid()) { - return write(error_message( - 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(error_message( - 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( - error_message(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(error_message( - QStringLiteral("input slot order mismatch"), - message.ticket_id)); - } - input_slots.append(int(consumed_slot)); - - const olive::ipc::FrameSlotMeta *meta = - input_pool_->meta(consumed_slot); - if (meta) { - } - } - } - - olive::VideoParams vparams( - message.width > 0 ? message.width : k_default_width, - message.height > 0 ? message.height : k_default_height, - olive::Rational(1, k_default_frame_rate), - message.format >= 0 ? olive::PixelFormat::Format(message.format) : - olive::PixelFormat::f32, - message.channel_count > 0 ? message.channel_count : - olive::VideoParams::k_rgba_channel_count); - - olive::RenderTicketPtr ticket = std::make_shared(); - ticket->setProperty("node", olive::QtUtils::ptr_to_value(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::k_type_video); - ticket->setProperty("colormanager", olive::QtUtils::ptr_to_value( - project_->color_manager())); - - { - olive::ColorProcessorPtr color_output; - if (message.has_color_transform) { - QString cache_key = QStringLiteral("%1|%2|%3|%4") - .arg(message.color_is_display ? 1 : 0) - .arg(message.color_output, - message.color_view, - message.color_look); - auto it = color_processor_cache_.find(cache_key); - if (it != color_processor_cache_.end()) { - color_output = it.value(); - } else { - 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()->get_reference_color_space(), - transform); - if (color_output) { - color_processor_cache_.insert(cache_key, color_output); - } - } - } - ticket->setProperty("coloroutput", - QVariant::fromValue(color_output)); - } - ticket->setProperty("vparam", QVariant::fromValue(vparams)); - // The IPC render_frame message carries no audio parameters, but - // rendering a sequence that has audio content evaluates audio - // tracks with globals.aparams -- an empty AudioParams aborts - // (AudioParams::time_to_samples asserts is_valid). Use the render - // node's own audio parameters, mirroring the in-process render - // path (PreviewAutoCacher uses context->get_audio_params()). - olive::AudioParams aparam; - if (olive::ViewerOutput *viewer = - dynamic_cast(node)) { - aparam = viewer->get_audio_params(); - } - ticket->setProperty("aparam", QVariant::fromValue(aparam)); - ticket->setProperty("return", olive::RenderManager::k_frame); - ticket->setProperty("cache", QString()); - ticket->setProperty("cachetimebase", - QVariant::fromValue(olive::Rational(1))); - ticket->setProperty("cacheid", QVariant::fromValue(QUuid())); - ticket->setProperty("multicam", olive::QtUtils::ptr_to_value( - static_cast(nullptr))); - ticket->setProperty( - "ipc_input_pool", - // The engine reads this back as the internal implementation object - // (olive::engine::internal::ipc::FrameSlotPool), which is exactly - // what the C handle points at. - olive::QtUtils::ptr_to_value(input_pool_ ? - static_cast( - input_pool_->handle()) : - 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->has_result()) { - return write(error_message( - QStringLiteral("render produced no frame"), message.ticket_id)); - } - - olive::FramePtr frame = ticket->get().value(); - if (!frame || !frame->is_allocated()) { - return write(error_message(QStringLiteral("render result was empty"), - message.ticket_id)); - } - - uint32_t slot = 0; - if (!output_pool_->acquire(&slot)) { - return write( - error_message(QStringLiteral("no free output frame slot"), - message.ticket_id)); - } - - const int data_size = frame->linesize_bytes() * frame->height(); - if (data_size > int(output_pool_->slot_data_bytes())) { - output_pool_->release(slot); - log_error(QString("Output frame size") + QString::number(data_size)); - log_error(QString("Slot size") + - QString::number(output_pool_->slot_data_bytes())); - return write(error_message( - QStringLiteral("rendered frame does not fit output slot "), - message.ticket_id)); - } - - std::memcpy(output_pool_->slot_data(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(error_message( - 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.to_json()); - } - - olive::Renderer *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_; - QHash color_processor_cache_; -}; - -} // namespace +#include "oakengine/worker.h" +// The render worker is a thin shell: all runtime logic (Qt application setup, +// render backend initialization, startup handshake and the NDJSON control +// loop) lives inside liboakengine behind the pure C ABI, so this executable +// imports no engine C++ symbols. int main(int argc, char *argv[]) { - QCoreApplication::setAttribute(Qt::AA_UseDesktopOpenGL); - QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); - install_surface_format(); - - QGuiApplication app(argc, argv); - -#ifdef Q_OS_MACOS - HideWorkerDockIcon(); -#endif - - QCoreApplication::setOrganizationName(QStringLiteral("oakvideoeditor.org")); - QCoreApplication::setApplicationName(QStringLiteral("oak-render-worker")); - - QString backend = QStringLiteral("opengl"); - const QStringList args = app.arguments(); - for (int i = 1; i < args.size(); ++i) { - if (args[i] == QStringLiteral("--backend") && i + 1 < args.size()) { - backend = args[i + 1].toLower(); - ++i; - } - } - -#ifdef Q_OS_LINUX - std::signal(SIGSEGV, print_backtrace); - std::signal(SIGABRT, print_backtrace); - std::signal(SIGFPE, print_backtrace); -#endif - - QFile in; - QFile out; - if (!in.open(stdin, QIODevice::ReadOnly | QIODevice::Unbuffered) || - !out.open(stdout, QIODevice::WriteOnly | QIODevice::Unbuffered)) { - log_error(QStringLiteral("failed to open stdio control pipes")); - return 1; - } - - olive::Renderer *renderer; -#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND - auto *dynamic_renderer = new olive::DynamicRenderer(backend); - if (dynamic_renderer->init()) { - dynamic_renderer->post_init(); - renderer = dynamic_renderer; - } else { - delete dynamic_renderer; - qWarning() << "Failed to initialize dynamic" << backend - << "backend, falling back to direct OpenGL renderer"; - renderer = new olive::OpenGLRenderer(); - if (!renderer->init()) { - log_error(QStringLiteral("failed to initialize OpenGL renderer")); - delete renderer; - return 1; - } - renderer->post_init(); - } -#else - renderer = new olive::OpenGLRenderer(); - if (!renderer->Init()) { - LogError(QStringLiteral("failed to initialize OpenGL renderer")); - delete renderer; - return 1; - } - renderer->PostInit(); -#endif - - // Validate the renderer. For OpenGL we check the GL context; for Vulkan we - // rely on Init()/PostInit() succeeding (there is no QOpenGLContext). - bool renderer_valid = true; - QOpenGLContext *ctx = nullptr; - if (backend == QStringLiteral("opengl")) { -#ifdef OAK_ENABLE_DYNAMIC_RENDER_BACKEND - if (auto *loaded_renderer = - dynamic_cast(renderer)) { - ctx = loaded_renderer->open_gl_context(); - } else -#endif - { - ctx = static_cast(renderer)->context(); - } - if (!ctx || !ctx->isValid()) { - renderer_valid = false; - } - } - if (!renderer_valid) { - log_error(QStringLiteral("OpenGL context is not valid after init")); - renderer->destroy(); - renderer->post_destroy(); - delete renderer; - return 1; - } - - int exit_code = 0; - { - RenderWorker worker(renderer, &out); - if (!worker.initialize_runtime() || !worker.send_startup_handshake()) { - 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::read_message(&buffer, &message, &ok)) { - if (!ok) { - olive::ipc::write_message( - &out, error_message(QStringLiteral( - "malformed control message"))); - out.flush(); - continue; - } - break; - } - - if (!worker.handle(message)) { - exit_code = 1; - break; - } - } - } - } - } - - renderer->destroy(); - renderer->post_destroy(); - delete renderer; - - return exit_code; + return oakengine_worker_main(argc, argv); } diff --git a/worker/workermain_mac.mm b/worker/workermain_mac.mm deleted file mode 100644 index 31b24547c..000000000 --- a/worker/workermain_mac.mm +++ /dev/null @@ -1,26 +0,0 @@ -/*** - - 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 - -void HideWorkerDockIcon() -{ - [NSApp setActivationPolicy:NSApplicationActivationPolicyProhibited]; -}