diff --git a/engine/common/filefunctions.cpp b/engine/common/filefunctions.cpp index 1c25b79c9..7a12621e9 100644 --- a/engine/common/filefunctions.cpp +++ b/engine/common/filefunctions.cpp @@ -55,6 +55,15 @@ QString FileFunctions::get_unique_file_identifier(const QString &filename) QString FileFunctions::get_configuration_location() { + // Tests and tooling can redirect the configuration (and, since most + // locations derive from it, the cache/data) root. XDG_* vars only work + // on Linux; QStandardPaths ignores them on macOS and Windows. + const QByteArray override_dir = qgetenv("OAK_CONFIG_DIR"); + if (!override_dir.isEmpty()) { + QDir(override_dir).mkpath("."); + return QString::fromUtf8(override_dir); + } + if (is_portable()) { return get_application_path(); } else { diff --git a/engine/node/output/track/track.cpp b/engine/node/output/track/track.cpp index f4c239c28..ca08e0a29 100644 --- a/engine/node/output/track/track.cpp +++ b/engine/node/output/track/track.cpp @@ -511,7 +511,9 @@ void Track::replace_block(Block *old, Block *replace) int cache_index = blocks_.indexOf(old); int index_of_old_block = get_array_index_from_cache_index(cache_index); + ignore_block_disconnect_++; disconnect_edge(old, NodeInput(this, k_block_input, index_of_old_block)); + ignore_block_disconnect_--; connect_edge(replace, NodeInput(this, k_block_input, index_of_old_block)); blocks_.replace(cache_index, replace); disconnect(old, &Block::length_changed, this, &Track::block_length_changed); @@ -584,6 +586,37 @@ void Track::InputConnectedEvent(const QString &input, int element, Node *node) } } +void Track::InputDisconnectedEvent(const QString &input, int element, + Node *output) +{ + Node::InputDisconnectedEvent(input, element, output); + + // Keep the block cache consistent when a block edge is removed outside + // the Track's own mutating operations (e.g. the block is being deleted, + // or an undo command is detaching the whole track from the graph). + // Without this, blocks_ keeps a dangling pointer and later readers such + // as track_length() walk into freed memory. + // + // Only the volatile cache is trimmed here; the persistent array map is + // deliberately left alone so that undo can re-attach the blocks from it + // (InputConnectedEvent rebuilds the cache while arraymap_invalid_ is + // set). + if (input == k_block_input && ignore_block_disconnect_ == 0) { + const int index = blocks_.indexOf(static_cast(output)); + if (index != -1) { + blocks_.removeAt(index); + block_array_indexes_.removeAt(index); + arraymap_invalid_ = true; + + Block *previous = (index > 0) ? blocks_.at(index - 1) : nullptr; + Block *next = (index < blocks_.size()) ? blocks_.at(index) : nullptr; + Block::set_previous_next(previous, next); + + update_in_out_from(index); + } + } +} + void Track::update_in_out_from(int index) { // Find block just before this one to find the last out point diff --git a/engine/node/output/track/track.h b/engine/node/output/track/track.h index 7a6e9fd71..d0f2fd514 100644 --- a/engine/node/output/track/track.h +++ b/engine/node/output/track/track.h @@ -460,6 +460,8 @@ signals: protected: virtual void InputConnectedEvent(const QString &input, int element, Node *node) override; + virtual void InputDisconnectedEvent(const QString &input, int element, + Node *output) override; virtual void InputValueChangedEvent(const QString &input, int element) override; @@ -501,6 +503,13 @@ private: bool arraymap_invalid_; bool ignore_arraymap_set_; + /** + * @brief Nestable guard suppressing the block-cache maintenance in + * InputDisconnectedEvent while the Track itself is rewiring block edges + * (e.g. replace_block), where the cache update is handled explicitly. + */ + int ignore_block_disconnect_ = 0; + private slots: void block_length_changed(); diff --git a/engine/render/renderworkerpool.cpp b/engine/render/renderworkerpool.cpp index 3bffd2c2d..68c6bd033 100644 --- a/engine/render/renderworkerpool.cpp +++ b/engine/render/renderworkerpool.cpp @@ -1141,6 +1141,17 @@ std::unique_ptr RenderWorkerPool::acquire_worker process->setProgram(worker_program_path()); process->setArguments({ QStringLiteral("--backend"), gpu_backend_ }); + // The engine defaults QT_QPA_PLATFORM to "offscreen" for headless hosts + // (cli/tests), but the worker needs a real platform GL context. Don't let + // it inherit the offscreen default from this process. + { + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + if (qEnvironmentVariable("QT_QPA_PLATFORM") == "offscreen") { + env.remove(QStringLiteral("QT_QPA_PLATFORM")); + process->setProcessEnvironment(env); + } + } + const QString worker_stderr_path = QDir(QDir::tempPath()) .filePath(QStringLiteral("oak-render-worker-%1-%2.stderr.log") diff --git a/engine/tests/oakengine_color_test.cpp b/engine/tests/oakengine_color_test.cpp index 1e6b065f1..a0c802bb5 100644 --- a/engine/tests/oakengine_color_test.cpp +++ b/engine/tests/oakengine_color_test.cpp @@ -396,6 +396,7 @@ TEST(OakEngineColor, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK); diff --git a/engine/tests/oakengine_events_test.cpp b/engine/tests/oakengine_events_test.cpp index 1a3f89092..f9f3e484b 100644 --- a/engine/tests/oakengine_events_test.cpp +++ b/engine/tests/oakengine_events_test.cpp @@ -922,6 +922,7 @@ TEST(OakEngineEvents, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK); diff --git a/engine/tests/oakengine_export_test.cpp b/engine/tests/oakengine_export_test.cpp index 17ed5187c..1200c982f 100644 --- a/engine/tests/oakengine_export_test.cpp +++ b/engine/tests/oakengine_export_test.cpp @@ -216,6 +216,7 @@ TEST(OakEngineExport, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK); diff --git a/engine/tests/oakengine_footage_test.cpp b/engine/tests/oakengine_footage_test.cpp index 5c90728e2..c16c10883 100644 --- a/engine/tests/oakengine_footage_test.cpp +++ b/engine/tests/oakengine_footage_test.cpp @@ -888,6 +888,7 @@ TEST(OakEngineFootage, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK); diff --git a/engine/tests/oakengine_init_test.cpp b/engine/tests/oakengine_init_test.cpp index 10fe1abe5..853da1cc2 100644 --- a/engine/tests/oakengine_init_test.cpp +++ b/engine/tests/oakengine_init_test.cpp @@ -312,6 +312,14 @@ static void test_footage_fixture(void) EXPECT_TRUE(n > 0 && (size_t)n < sizeof(fixture)); EXPECT_TRUE(file_exists(fixture)); + // The fixture stores footage as the relative path "demo.mp4". Run the + // load from the fixture directory so the engine does not treat the + // project as "moved" and rewrite the stored filename to an absolute + // path (see EngineCore footage relocation on load). + char fixture_dir[4096]; + snprintf(fixture_dir, sizeof(fixture_dir), "%s/tests", OAK_TEST_SOURCE_DIR); + EXPECT_TRUE(chdir(fixture_dir) == 0); + OakEngineProject *p = oakengine_project_create(); EXPECT_TRUE(p != NULL); char err[512]; @@ -417,6 +425,7 @@ TEST(OakEngineInit, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif test_init(); diff --git a/engine/tests/oakengine_keyframe_test.cpp b/engine/tests/oakengine_keyframe_test.cpp index a6482f87c..1c56a5adb 100644 --- a/engine/tests/oakengine_keyframe_test.cpp +++ b/engine/tests/oakengine_keyframe_test.cpp @@ -831,6 +831,7 @@ TEST(OakEngineKeyframe, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK); diff --git a/engine/tests/oakengine_node_test.cpp b/engine/tests/oakengine_node_test.cpp index f214f23f1..657063475 100644 --- a/engine/tests/oakengine_node_test.cpp +++ b/engine/tests/oakengine_node_test.cpp @@ -1050,6 +1050,7 @@ TEST(OakEngineNode, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK); diff --git a/engine/tests/oakengine_playback_test.cpp b/engine/tests/oakengine_playback_test.cpp index 51e4fbc2d..d74390a48 100644 --- a/engine/tests/oakengine_playback_test.cpp +++ b/engine/tests/oakengine_playback_test.cpp @@ -300,6 +300,7 @@ TEST(OakEnginePlayback, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif // HEADLESS is enough for the validation part and creates the diff --git a/engine/tests/oakengine_preview_test.cpp b/engine/tests/oakengine_preview_test.cpp index 1f1b4b0ea..61874d49c 100644 --- a/engine/tests/oakengine_preview_test.cpp +++ b/engine/tests/oakengine_preview_test.cpp @@ -244,6 +244,7 @@ TEST(OakEnginePreview, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK); diff --git a/engine/tests/oakengine_renderer_test.cpp b/engine/tests/oakengine_renderer_test.cpp index fe7e1449d..c812e4433 100644 --- a/engine/tests/oakengine_renderer_test.cpp +++ b/engine/tests/oakengine_renderer_test.cpp @@ -240,6 +240,7 @@ TEST(OakEngineRenderer, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif // HEADLESS is enough for the validation part and creates the offscreen diff --git a/engine/tests/oakengine_sync_test.cpp b/engine/tests/oakengine_sync_test.cpp index c54d0d5fb..00f67ff47 100644 --- a/engine/tests/oakengine_sync_test.cpp +++ b/engine/tests/oakengine_sync_test.cpp @@ -212,6 +212,7 @@ TEST(OakEngineSync, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif // HEADLESS is enough for the validation part. diff --git a/engine/tests/oakengine_timeline_edit_test.cpp b/engine/tests/oakengine_timeline_edit_test.cpp index 295470b3e..04a73e140 100644 --- a/engine/tests/oakengine_timeline_edit_test.cpp +++ b/engine/tests/oakengine_timeline_edit_test.cpp @@ -1806,6 +1806,7 @@ TEST(OakEngineTimelineEdit, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK); diff --git a/engine/tests/oakengine_traverse_test.cpp b/engine/tests/oakengine_traverse_test.cpp index 78c0b26d1..5ed15b19e 100644 --- a/engine/tests/oakengine_traverse_test.cpp +++ b/engine/tests/oakengine_traverse_test.cpp @@ -250,6 +250,7 @@ TEST(OakEngineTraverse, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK); diff --git a/engine/tests/oakengine_viewer_test.cpp b/engine/tests/oakengine_viewer_test.cpp index ec47fc7db..fe7df4bd5 100644 --- a/engine/tests/oakengine_viewer_test.cpp +++ b/engine/tests/oakengine_viewer_test.cpp @@ -552,6 +552,7 @@ TEST(OakEngineViewer, Main) EXPECT_TRUE(setenv("XDG_CONFIG_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_CACHE_HOME", g_tmpdir, 1) == 0); EXPECT_TRUE(setenv("XDG_DATA_HOME", g_tmpdir, 1) == 0); + EXPECT_TRUE(setenv("OAK_CONFIG_DIR", g_tmpdir, 1) == 0); #endif EXPECT_TRUE(oakengine_init(OAKENGINE_INIT_HEADLESS) == OAKENGINE_OK);