From ed5afbb5cc4f33d70eeff6662bb54d52b3430ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sun, 5 Jul 2026 17:28:07 -0400 Subject: [PATCH 01/10] core: add ObjectPath::fromString / findObject, reuse in remote control --- src/lib/score/model/path/ObjectPath.cpp | 24 ++++++++++++++ src/lib/score/model/path/ObjectPath.hpp | 2 ++ .../Websockets/DocumentPlugin.cpp | 32 +------------------ 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/lib/score/model/path/ObjectPath.cpp b/src/lib/score/model/path/ObjectPath.cpp index 6fd28de16a..e9e19e2e2f 100644 --- a/src/lib/score/model/path/ObjectPath.cpp +++ b/src/lib/score/model/path/ObjectPath.cpp @@ -147,6 +147,30 @@ QString ObjectPath::toString() const noexcept return s; } +ObjectPath ObjectPath::fromString(const QString& str) noexcept +{ + std::vector v; + for(const auto& seg : QStringView{str}.split(QLatin1Char('/'), Qt::SkipEmptyParts)) + { + const auto dot = seg.lastIndexOf(QLatin1Char('.')); + if(dot <= 0) + return {}; + bool ok{}; + const int32_t id = seg.mid(dot + 1).toInt(&ok); + if(!ok) + return {}; + v.emplace_back(seg.left(dot).toString(), id); + } + return ObjectPath{std::move(v)}; +} + +QObject* ObjectPath::findObject(const score::DocumentContext& ctx) const noexcept +{ + if(m_objectIdentifiers.empty()) + return nullptr; + return find_impl_unsafe(ctx); +} + QObject* ObjectPath::find_impl(const score::DocumentContext& ctx) const { using namespace score; diff --git a/src/lib/score/model/path/ObjectPath.hpp b/src/lib/score/model/path/ObjectPath.hpp index 963c39efe6..7b8e45a782 100644 --- a/src/lib/score/model/path/ObjectPath.hpp +++ b/src/lib/score/model/path/ObjectPath.hpp @@ -59,6 +59,8 @@ class SCORE_LIB_BASE_EXPORT ObjectPath ObjectPath() noexcept { } ~ObjectPath() noexcept = default; QString toString() const noexcept; + static ObjectPath fromString(const QString& str) noexcept; + QObject* findObject(const score::DocumentContext& ctx) const noexcept; explicit ObjectPath(std::vector vec) noexcept : m_objectIdentifiers{std::move(vec)} diff --git a/src/plugins/score-plugin-remotecontrol/RemoteControl/Websockets/DocumentPlugin.cpp b/src/plugins/score-plugin-remotecontrol/RemoteControl/Websockets/DocumentPlugin.cpp index 1a2222dc9c..63075ee32f 100644 --- a/src/plugins/score-plugin-remotecontrol/RemoteControl/Websockets/DocumentPlugin.cpp +++ b/src/plugins/score-plugin-remotecontrol/RemoteControl/Websockets/DocumentPlugin.cpp @@ -131,36 +131,6 @@ void DocumentPlugin::cleanup() m_root = nullptr; } -ObjectPath fromString(const QString& str) -{ - auto res = str.split("/"); - if(res.empty()) - return {}; - if(res[0].isEmpty()) - res.pop_front(); - if(res.empty()) - return {}; - - ObjectIdentifierVector i; - for(const QString& is : res) - { - int idx = is.lastIndexOf('.'); - if(idx == -1) - return {}; - auto name = is.mid(0, idx); - auto index = is.mid(idx + 1); - - bool ok = false; - int num = index.toInt(&ok); - if(!ok) - return {}; - - i.emplace_back(name, num); - } - - return ObjectPath{std::move(i)}; -} - template static Path readPathFromValue(const rapidjson::Value& val) { @@ -168,7 +138,7 @@ static Path readPathFromValue(const rapidjson::Value& val) { auto str = QString::fromUtf8(val.GetString(), val.GetStringLength()); - auto path = fromString(str); + auto path = ObjectPath::fromString(str); if(path.vec().empty()) return {}; From b1b0062413ac895aa3083b9496df46b2597375a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sun, 5 Jul 2026 17:28:15 -0400 Subject: [PATCH 02/10] js: add Score.cable, path/findByPath, cable removal, setAutoTrigger/setProcessLoop --- .../score-plugin-js/JS/Qml/EditContext.cpp | 27 +++++++++++++ .../score-plugin-js/JS/Qml/EditContext.hpp | 13 ++++++ .../JS/Qml/EditContext.port.cpp | 23 +++++++++++ .../JS/Qml/EditContext.scenario.cpp | 40 +++++++++++++++++++ 4 files changed, 103 insertions(+) diff --git a/src/plugins/score-plugin-js/JS/Qml/EditContext.cpp b/src/plugins/score-plugin-js/JS/Qml/EditContext.cpp index 552ce6b2dd..51ec36979a 100644 --- a/src/plugins/score-plugin-js/JS/Qml/EditContext.cpp +++ b/src/plugins/score-plugin-js/JS/Qml/EditContext.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -9,6 +10,7 @@ #include #include +#include #include #include @@ -81,6 +83,31 @@ QObject* EditJsContext::findByLabel(QString p) return nullptr; } +QString EditJsContext::path(QObject* obj) +{ + auto doc = ctx(); + if(!doc || !obj) + return {}; + try + { + auto full = ObjectPath::pathBetweenObjects(&doc->document.model(), obj); + auto& v = full.vec(); + return ObjectPath{{v.begin() + 1, v.end()}}.toString(); + } + catch(...) + { + return {}; + } +} + +QObject* EditJsContext::findByPath(QString path) +{ + auto doc = ctx(); + if(!doc) + return nullptr; + return ObjectPath::fromString(path).findObject(*doc); +} + void EditJsContext::load(QString doc) { auto& documents = score::GUIAppContext().docManager; diff --git a/src/plugins/score-plugin-js/JS/Qml/EditContext.hpp b/src/plugins/score-plugin-js/JS/Qml/EditContext.hpp index 99f2820a47..76ef0beef3 100644 --- a/src/plugins/score-plugin-js/JS/Qml/EditContext.hpp +++ b/src/plugins/score-plugin-js/JS/Qml/EditContext.hpp @@ -142,6 +142,12 @@ class SCORE_PLUGIN_JS_EXPORT EditJsContext : public QObject void setIntervalSpeed(QObject* object, double); W_SLOT(setIntervalSpeed) + void setAutoTrigger(QObject* timeSync, bool); + W_SLOT(setAutoTrigger) + + void setProcessLoop(QObject* process, bool); + W_SLOT(setProcessLoop) + QObject* port(QObject* obj, QString name); W_SLOT(port) @@ -162,6 +168,9 @@ class SCORE_PLUGIN_JS_EXPORT EditJsContext : public QObject QObject* createCable(QObject* outlet, QObject* inlet, Process::CableType type); W_SLOT(createCable, (QObject*, QObject*, Process::CableType)) + QObject* cable(QObject* outlet, QObject* inlet); + W_SLOT(cable) + void setAddress(QObject* obj, QString addr); W_SLOT(setAddress) @@ -284,6 +293,10 @@ class SCORE_PLUGIN_JS_EXPORT EditJsContext : public QObject QObject* findByLabel(QString p); W_SLOT(findByLabel) + QString path(QObject* obj); + W_SLOT(path) + QObject* findByPath(QString path); + W_SLOT(findByPath) QObject* document(); W_SLOT(document) diff --git a/src/plugins/score-plugin-js/JS/Qml/EditContext.port.cpp b/src/plugins/score-plugin-js/JS/Qml/EditContext.port.cpp index bb1549e8cf..035958a46c 100644 --- a/src/plugins/score-plugin-js/JS/Qml/EditContext.port.cpp +++ b/src/plugins/score-plugin-js/JS/Qml/EditContext.port.cpp @@ -128,6 +128,29 @@ EditJsContext::createCable(QObject* outlet, QObject* inlet, Process::CableType t return &c; } +// Find the cable currently connecting a given outlet to a given inlet, or null. +// Derives connection state from the live graph (cables are unnamed and their +// ids are reused, so they cannot be tracked reliably by name or path). +QObject* EditJsContext::cable(QObject* outlet, QObject* inlet) +{ + auto doc = ctx(); + if(!doc) + return nullptr; + auto src = qobject_cast(outlet); + auto sink = qobject_cast(inlet); + if(!src || !sink) + return nullptr; + + auto& root = score::IDocument::get(doc->document); + auto& ctx = doc->document.context(); + for(auto& c : root.cables) + { + if(c.source().try_find(ctx) == src && c.sink().try_find(ctx) == sink) + return &c; + } + return nullptr; +} + void EditJsContext::setAddress(QObject* obj, QString addr) { auto doc = ctx(); diff --git a/src/plugins/score-plugin-js/JS/Qml/EditContext.scenario.cpp b/src/plugins/score-plugin-js/JS/Qml/EditContext.scenario.cpp index 122f5b914a..6166029a51 100644 --- a/src/plugins/score-plugin-js/JS/Qml/EditContext.scenario.cpp +++ b/src/plugins/score-plugin-js/JS/Qml/EditContext.scenario.cpp @@ -1,5 +1,8 @@ +#include +#include #include #include +#include #include #include @@ -485,6 +488,15 @@ void EditJsContext::remove(QObject* obj) if(auto itv = qobject_cast(proc->parent())) m->removeProcess(*itv, proc->id()); } + else if(auto cable = qobject_cast(obj)) + { + // Cables live in the document-level cable map, not in a scenario process, + // so the generic parent-based removal below never matches them. + auto& root + = score::IDocument::get(doc->document); + auto [m, _] = macro(*doc); + m->removeCable(root, *cable); + } else if(auto p = obj->parent()) { if(auto scenar = qobject_cast(p)) @@ -635,4 +647,32 @@ void EditJsContext::setIntervalSpeed(QObject* object, double s) i->duration.setSpeed(s); } + +void EditJsContext::setAutoTrigger(QObject* object, bool b) +{ + auto doc = ctx(); + if(!doc) + return; + + auto ts = qobject_cast(object); + if(!ts) + return; + + auto [m, _] = macro(*doc); + m->setProperty(*ts, b); +} + +void EditJsContext::setProcessLoop(QObject* object, bool b) +{ + auto doc = ctx(); + if(!doc) + return; + + auto proc = qobject_cast(object); + if(!proc) + return; + + auto [m, _] = macro(*doc); + m->setProperty(*proc, b); +} } From 42e44a53309b4f2bb1b3c6a3fcc13d79a3256986 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sun, 5 Jul 2026 17:28:22 -0400 Subject: [PATCH 03/10] gfx: clear image inputs with no incoming edge each frame --- .../score-plugin-gfx/Gfx/Graph/RenderList.cpp | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.cpp index 0d5613bdb4..92d1d66173 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/RenderList.cpp @@ -1303,7 +1303,38 @@ void RenderList::render(QRhiCommandBuffer& commands, bool force) // For each edge incoming to each image input ports of this node, // we render the edge source's content. if(input->edges.empty()) + { + // An image input with no incoming edge must still have its render + // target cleared each frame; otherwise removing a cable while both + // endpoints stay alive leaves the last frame stuck in the consumer + // (e.g. a mixer keeps compositing a source that was disconnected). + if(input->type == Types::Image + && (input->flags & Flag::GrabsFromSource) != Flag::GrabsFromSource) + { + auto rendered = node->renderedNodes.find(this); + SCORE_ASSERT(rendered != node->renderedNodes.end()); + NodeRenderer* renderer = rendered->second; + + auto rt = renderer->renderTargetForInput(*input); + if(!rt) + rt = renderTargetForInputPort(*input); + if(rt) + { + QColor bg = (it + 1 == this->nodes.rend() ? Qt::black : Qt::transparent); + commands.beginPass(rt.renderTarget, bg, {1.0f, 0}, updateBatch); + updateBatch = nullptr; + commands.endPass(updateBatch); + updateBatch = nullptr; + + if(node != &output) + { + updateBatch = state.rhi->nextResourceUpdateBatch(); + SCORE_ASSERT(updateBatch); + } + } + } continue; + } if(input->type == Types::Image) { From ee1a189f9dd71b5c1e877528a30392f9a322ca06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sun, 5 Jul 2026 17:28:22 -0400 Subject: [PATCH 04/10] gfx: don't return a half-initialized render state when QRhi::create fails --- .../score-plugin-gfx/Gfx/Graph/ScreenNode.cpp | 45 ++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.cpp index 2ae89c529e..36ff58a645 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.cpp @@ -238,9 +238,12 @@ createRenderState(GraphicsApi graphicsApi, QSize sz, QWindow* window) params.format.setSamples(state.samples); state.version = caps.qShaderVersion; state.rhi = QRhi::create(QRhi::OpenGLES2, ¶ms, flags); - state.renderSize = sz; - populateCaps(state); - return st; + if(state.rhi) + { + state.renderSize = sz; + populateCaps(state); + return st; + } } #endif @@ -331,9 +334,12 @@ createRenderState(GraphicsApi graphicsApi, QSize sz, QWindow* window) if(!state.rhi) state.rhi = QRhi::create(QRhi::Vulkan, ¶ms, flags); - state.renderSize = sz; - populateCaps(state); - return st; + if(state.rhi) + { + state.renderSize = sz; + populateCaps(state); + return st; + } } #endif @@ -351,9 +357,12 @@ createRenderState(GraphicsApi graphicsApi, QSize sz, QWindow* window) // } state.version = Gfx::Settings::shaderVersionForAPI(D3D11); state.rhi = QRhi::create(QRhi::D3D11, ¶ms, flags); - state.renderSize = sz; - populateCaps(state); - return st; + if(state.rhi) + { + state.renderSize = sz; + populateCaps(state); + return st; + } } #if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) else if(graphicsApi == D3D12) @@ -369,9 +378,12 @@ createRenderState(GraphicsApi graphicsApi, QSize sz, QWindow* window) // } state.version = Gfx::Settings::shaderVersionForAPI(D3D12); state.rhi = QRhi::create(QRhi::D3D12, ¶ms, flags); - state.renderSize = sz; - populateCaps(state); - return st; + if(state.rhi) + { + state.renderSize = sz; + populateCaps(state); + return st; + } } #endif #endif @@ -382,9 +394,12 @@ createRenderState(GraphicsApi graphicsApi, QSize sz, QWindow* window) QRhiMetalInitParams params; state.version = Gfx::Settings::shaderVersionForAPI(Metal); state.rhi = QRhi::create(QRhi::Metal, ¶ms, flags); - state.renderSize = sz; - populateCaps(state); - return st; + if(state.rhi) + { + state.renderSize = sz; + populateCaps(state); + return st; + } } #endif From 295a0e452ba5d74ddd79f8f776dfb89185b0a73a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sun, 5 Jul 2026 17:28:29 -0400 Subject: [PATCH 05/10] core: disconnect devices before tearing down documents --- src/lib/core/presenter/DocumentManager.cpp | 11 +++++++++++ .../Explorer/DocumentPlugin/DeviceDocumentPlugin.cpp | 8 ++++++++ .../Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp | 2 ++ 3 files changed, 21 insertions(+) diff --git a/src/lib/core/presenter/DocumentManager.cpp b/src/lib/core/presenter/DocumentManager.cpp index f22aab74f7..2d27de53f9 100644 --- a/src/lib/core/presenter/DocumentManager.cpp +++ b/src/lib/core/presenter/DocumentManager.cpp @@ -156,6 +156,17 @@ DocumentManager::~DocumentManager() // The documents have to be deleted before the application context plug-ins. // This is because the Local device has to be deleted last in // ApplicationPlugin. + for(auto document : m_documents) + { + // Reverse creation order, to match ~DocumentModel (e.g. the execution + // plugin must be torn down before the device explorer plugin). + auto& plugs = document->model().pluginModels(); + for(auto it = plugs.rbegin(); it != plugs.rend(); ++it) + { + (*it)->on_documentClosing(); + } + } + for(auto document : m_documents) { document->deleteLater(); diff --git a/src/plugins/score-plugin-deviceexplorer/Explorer/DocumentPlugin/DeviceDocumentPlugin.cpp b/src/plugins/score-plugin-deviceexplorer/Explorer/DocumentPlugin/DeviceDocumentPlugin.cpp index eb1f5bae19..a2aee645d6 100644 --- a/src/plugins/score-plugin-deviceexplorer/Explorer/DocumentPlugin/DeviceDocumentPlugin.cpp +++ b/src/plugins/score-plugin-deviceexplorer/Explorer/DocumentPlugin/DeviceDocumentPlugin.cpp @@ -59,6 +59,14 @@ DeviceDocumentPlugin::DeviceDocumentPlugin( m_explorer = new DeviceExplorerModel{*this, this}; } +void DeviceDocumentPlugin::on_documentClosing() +{ + for(auto dev : this->m_list.devices()) + { + dev->disconnect(); + } +} + DeviceDocumentPlugin::~DeviceDocumentPlugin() { for(auto dev : this->m_list.devices()) diff --git a/src/plugins/score-plugin-deviceexplorer/Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp b/src/plugins/score-plugin-deviceexplorer/Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp index 0a42415044..c2b85985fb 100644 --- a/src/plugins/score-plugin-deviceexplorer/Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp +++ b/src/plugins/score-plugin-deviceexplorer/Explorer/DocumentPlugin/DeviceDocumentPlugin.hpp @@ -49,6 +49,8 @@ class SCORE_PLUGIN_DEVICEEXPLORER_EXPORT DeviceDocumentPlugin final void init(); + void on_documentClosing() override; + Device::Node& rootNode() { return m_rootNode; } const Device::Node& rootNode() const { return m_rootNode; } From 25d183b4252e7c484288f95cdc3cdcabc0c60568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sun, 5 Jul 2026 17:28:29 -0400 Subject: [PATCH 06/10] gfx: release the static Vulkan instance when creation fails --- src/lib/score/gfx/Vulkan.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/score/gfx/Vulkan.cpp b/src/lib/score/gfx/Vulkan.cpp index 98a5baceeb..309daca8c3 100644 --- a/src/lib/score/gfx/Vulkan.cpp +++ b/src/lib/score/gfx/Vulkan.cpp @@ -60,6 +60,8 @@ QVulkanInstance* staticVulkanInstance(bool create) if(!instance.create()) { g_staticVulkanInstanceInvalid = true; + delete g_staticVulkanInstance; + g_staticVulkanInstance = nullptr; } }); From cb5ca64ba99851dd83daecd7c5ccb79defd982b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Micha=C3=ABl=20Celerier?= Date: Sun, 5 Jul 2026 17:28:29 -0400 Subject: [PATCH 07/10] gstreamer: classify appsinks from the pipeline string, follow the engine quantum --- .../Gfx/GStreamer/GStreamerDevice.cpp | 174 +++++++++++++++--- 1 file changed, 148 insertions(+), 26 deletions(-) diff --git a/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerDevice.cpp b/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerDevice.cpp index 1df3e8e69e..94e5bd9f8d 100644 --- a/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerDevice.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/GStreamer/GStreamerDevice.cpp @@ -35,8 +35,10 @@ extern "C" { #include #include