diff --git a/3rdparty/libossia b/3rdparty/libossia index 7c0a1a62e4..c6b7edde4e 160000 --- a/3rdparty/libossia +++ b/3rdparty/libossia @@ -1 +1 @@ -Subproject commit 7c0a1a62e45cc0380e94ddfa483f6698adee305d +Subproject commit c6b7edde4e63ab2a170efc02be8919524c1e5059 diff --git a/ci/common.deps.sh b/ci/common.deps.sh index 2eecb19c18..2b6595e70e 100755 --- a/ci/common.deps.sh +++ b/ci/common.deps.sh @@ -34,7 +34,7 @@ CI_PLATFORM="${1:-DEFAULT}" if [[ "$CI_PLATFORM" != "WASM" ]]; then clone_addon https://github.com/ossia/score-addon-ltc - clone_addon https://github.com/ossia/score-addon-ndi +# clone_addon https://github.com/ossia/score-addon-ndi clone_addon https://github.com/bltzr/score-avnd-granola clone_addon https://github.com/ossia/score-addon-ultraleap clone_addon https://github.com/ossia/score-addon-contextfree diff --git a/src/app/Application.cpp b/src/app/Application.cpp index e59bf6316d..619c996bb9 100644 --- a/src/app/Application.cpp +++ b/src/app/Application.cpp @@ -230,6 +230,17 @@ Application::Application(int& argc, char** argv) } m_app = createApplication(appSettings, argc, argv); + +#if defined(QT_FEATURE_thread) +#if QT_FEATURE_thread == 1 + this->thread()->setPriority(QThread::Priority::TimeCriticalPriority); + QThreadPool::globalInstance()->setMaxThreadCount(2); + QThreadPool::globalInstance()->setThreadPriority(QThread::Priority::HighPriority); +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + QThreadPool::globalInstance()->setServiceLevel(QThread::QualityOfService::High); +#endif +#endif +#endif } Application::Application( @@ -445,7 +456,12 @@ void Application::init() #if defined(QT_FEATURE_thread) #if QT_FEATURE_thread == 1 + this->thread()->setPriority(QThread::Priority::TimeCriticalPriority); QThreadPool::globalInstance()->setMaxThreadCount(2); + QThreadPool::globalInstance()->setThreadPriority(QThread::Priority::HighPriority); +#if QT_VERSION >= QT_VERSION_CHECK(6, 9, 0) + QThreadPool::globalInstance()->setServiceLevel(QThread::QualityOfService::High); +#endif #endif #endif diff --git a/src/app/main.cpp b/src/app/main.cpp index fa8ec9af08..610a3ea73d 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -408,7 +408,13 @@ struct increase_timer_precision // Then maybe we can go a bit lower... ULONG currentRes{}; - NtSetTimerResolution(100, TRUE, ¤tRes); + NtSetTimerResolution(7500, TRUE, ¤tRes); + if(currentRes <= 8000) + NtSetTimerResolution(5000, TRUE, ¤tRes); + if(currentRes <= 6000) + NtSetTimerResolution(2500, TRUE, ¤tRes); + if(currentRes <= 3000) + NtSetTimerResolution(1000, TRUE, ¤tRes); #endif } diff --git a/src/lib/CMakeLists.txt b/src/lib/CMakeLists.txt index d2a4d24411..49ba4b9eb6 100755 --- a/src/lib/CMakeLists.txt +++ b/src/lib/CMakeLists.txt @@ -237,6 +237,7 @@ set(HEADERS "${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Unused.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/score/tools/RecursiveWatch.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/score/tools/ThreadPool.hpp" + "${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Timers.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Debug.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Version.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/score/tools/exceptions/MissingCommand.hpp" @@ -511,6 +512,7 @@ set(SRCS "${CMAKE_CURRENT_SOURCE_DIR}/score/widgets/SpinBoxes.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/score/tools/ListNetworkAddresses.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/score/tools/Timers.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/core/application/SafeQApplication.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/core/application/ApplicationInterface.cpp" diff --git a/src/lib/score/gfx/Vulkan.cpp b/src/lib/score/gfx/Vulkan.cpp index 59a90573fc..800fb605cf 100644 --- a/src/lib/score/gfx/Vulkan.cpp +++ b/src/lib/score/gfx/Vulkan.cpp @@ -3,6 +3,16 @@ #if defined(QT_FEATURE_vulkan) && QT_CONFIG(vulkan) && __has_include() #include +#if __has_include() +#include +#endif + +#if __has_include() +#include +#elif __has_include() +#include +#endif + #include namespace score::gfx { @@ -27,7 +37,7 @@ QVulkanInstance* staticVulkanInstance(bool create) #endif QByteArrayList exts; - exts << "VK_KHR_get_physical_device_properties2"; + exts << QRhiVulkanInitParams::preferredInstanceExtensions(); if(auto v = instance.supportedApiVersion(); v >= QVersionNumber(1, 1)) { diff --git a/src/lib/score/tools/Timers.cpp b/src/lib/score/tools/Timers.cpp new file mode 100644 index 0000000000..d092b28fd9 --- /dev/null +++ b/src/lib/score/tools/Timers.cpp @@ -0,0 +1,300 @@ +#include "Timers.hpp" +#include +#include +#include +#include +#include +#include +#include + +#include + +W_OBJECT_IMPL(score::HighResolutionTimer) +W_OBJECT_IMPL(score::Timers) + +namespace score +{ + +#if defined(HRT_STATS) +struct TimerStats { + double m2 = 0; + double mean = 0; + + double actualFrequencyHz = 0; + double jitterUs = 0; + double maxLatencyUs = 0; + uint64_t tickCount = 0; + uint64_t missedTicks = 0; + void tick(uint64_t actualNs, uint64_t nextTickNs, uint64_t lastTickNs, uint64_t startTimeNs, uint64_t intervalNs) + { + tickCount++; + + // Calculate actual interval and jitter using Welford's algorithm + double actualIntervalNs = static_cast(actualNs - lastTickNs); + double delta = actualIntervalNs - mean; + mean += delta / tickCount; + double delta2 = actualIntervalNs - mean; + m2 += delta * delta2; + + if (tickCount > 1) + { + const double variance = m2 / (tickCount - 1); + jitterUs = std::sqrt(variance) / 1000.0; + } + + // Latency + int64_t latencyNs = actualNs - nextTickNs; + if (latencyNs > 0) + { + double latencyUs = latencyNs / 1000.0; + if (latencyUs > maxLatencyUs) + maxLatencyUs = latencyUs; + } + + // Actual frequency + double elapsedS = (actualNs - startTimeNs) / 1'000'000'000.0; + if (elapsedS > 0) + actualFrequencyHz = tickCount / elapsedS; + + // Check for missed ticks + if (latencyNs > static_cast(intervalNs)) + { + missedTicks += latencyNs / intervalNs; + } + + if(tickCount % 10 == 0 && actualFrequencyHz > 90) { + qDebug() << " -- actualFrequencyHz:" << actualFrequencyHz + << " ; missed:" << missedTicks + << " ; jitterUs:" << jitterUs + << " ; maxLatencyUs:" << maxLatencyUs + << " ; tickCount:" << tickCount; + } + } +} g_mainThreadStats; + +uint64_t nextTickNs; +uint64_t lastTickNs; +uint64_t startTimeNs; +#endif + +class HighResolutionTimerPrivate +{ +public: + HighResolutionTimer* q{}; + QThread thread; + std::atomic running{false}; + double frequencyHz; + uint64_t intervalNs; + + int timerId{}; // For timerEvent + bool accurate{}; + + HighResolutionTimerPrivate(HighResolutionTimer* parent, double freq) + : q(parent) + , frequencyHz(freq) + , intervalNs(static_cast(1'000'000'000.0 / freq)) + { + thread.setObjectName(QStringLiteral("ossia-timer")); + } + + void timerLoop() + { + ossia::priority_boost_handle priorityHandle{frequencyHz}; + + uint64_t nextTickNs = ossia::now_ns() + intervalNs; +#if defined(HRT_STATS) + uint64_t lastTickNs = ossia::now_ns(); + uint64_t startTimeNs = lastTickNs; + TimerStats stats{}; +#endif + +#if defined(_WIN32) + ossia::windows_timer_sleep sleeper; +#else + ossia::adaptive_sleep sleeper; +#endif + while (running.load(std::memory_order_relaxed)) + { + sleeper.sleep_until(nextTickNs); + + uint64_t actualNs = ossia::now_ns(); + + // Emit signal + q->timeout(q); + +#if defined(HRT_STATS) + stats.tick(actualNs, nextTickNs, lastTickNs, startTimeNs, intervalNs); + lastTickNs = actualNs; +#endif + + // Schedule next tick + nextTickNs += intervalNs; + + // If we've fallen too far behind, reset + if (actualNs > nextTickNs + intervalNs * 3) + { + nextTickNs = actualNs + intervalNs; + } + } + } +}; + +HighResolutionTimer::HighResolutionTimer(double frequencyHz, QObject* parent) + : QObject(parent) + , d(std::make_unique(this, frequencyHz)) +{ +} + +HighResolutionTimer::~HighResolutionTimer() +{ + stop(); +} + +void HighResolutionTimer::timerEvent(QTimerEvent *k) +{ +#if defined(HRT_STATS) + uint64_t actualNs = ossia::now_ns(); + auto intervalNs = static_cast(1'000'000'000.0 / frequency()); + g_mainThreadStats.tick(actualNs, nextTickNs, lastTickNs, startTimeNs, intervalNs); + + lastTickNs = actualNs; + + // Schedule next tick + nextTickNs += intervalNs; + + // If we've fallen too far behind, reset + if (actualNs > nextTickNs + intervalNs * 3) + { + nextTickNs = actualNs + intervalNs; + } +#endif + timeout(this); +} + +void HighResolutionTimer::start() +{ + if(!d->accurate) + { + d->timerId = this->startTimer(std::chrono::milliseconds{int(1000. / this->frequency())}, Qt::PreciseTimer); + +#if defined(HRT_STATS) + nextTickNs = ossia::now_ns() + static_cast(1'000'000'000.0 / frequency()); + lastTickNs = ossia::now_ns(); + startTimeNs = lastTickNs; +#endif + } + else + { + if (d->running.exchange(true)) + return; + + QObject::connect(&d->thread, &QThread::started, [this]() { + d->timerLoop(); + }); + + d->thread.start(QThread::NormalPriority); + } + started(this); +} + +void HighResolutionTimer::stop() +{ + if(!d->accurate) + { + if (!d->running.exchange(false)) + return; + + d->thread.quit(); + d->thread.wait(); + + QObject::disconnect(&d->thread, &QThread::started, nullptr, nullptr); + } + stopped(this); +} + +bool HighResolutionTimer::isRunning() const +{ + return d->running.load(); +} + +double HighResolutionTimer::frequency() const +{ + return d->frequencyHz; +} + +void HighResolutionTimer::setMaximumAccuracy(bool b) +{ + if(b != d->accurate) + { + d->accurate = b; + QSignalBlocker blocker{*this}; + stop(); + start(); + } +} + +Timers& Timers::instance() +{ + static Timers s_instance; + return s_instance; +} + +Timers::Timers(QObject* parent) + : QObject(parent) +{ +} + +Timers::~Timers() +{ + std::lock_guard lock{m_mutex}; + for (auto& [k, entry] : m_timers) + { + entry.timer->stop(); + } + m_timers.clear(); +} + +HighResolutionTimer* Timers::acquireTimer(QObject* user, double frequencyHz) +{ + std::lock_guard lock{m_mutex}; + if (auto it = m_timers.find(frequencyHz); it != m_timers.end()) + { + it->second.users[user]++; + return it->second.timer.get(); + } + + TimerEntry entry; + entry.timer = std::make_unique(frequencyHz, this); + entry.users[user] = 1; + entry.timer->start(); + + auto* ptr = entry.timer.get(); + m_timers.emplace(frequencyHz, std::move(entry)); + return ptr; +} + +void Timers::releaseTimer(QObject* user, HighResolutionTimer* timer) +{ + if(!timer) + return; + + std::lock_guard lock{m_mutex}; + for (auto it = m_timers.begin(); it != m_timers.end(); ++it) + { + if (it->second.timer.get() == timer) + { + if (--it->second.users[user] == 0) + { + disconnect(it->second.timer.get(), nullptr, user, nullptr); + it->second.users.erase(user); + if(it->second.users.empty()) { + it->second.timer->stop(); + m_timers.erase(it); + } + } + return; + } + } +} + +} diff --git a/src/lib/score/tools/Timers.hpp b/src/lib/score/tools/Timers.hpp new file mode 100644 index 0000000000..3acea2bbda --- /dev/null +++ b/src/lib/score/tools/Timers.hpp @@ -0,0 +1,62 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace score +{ +class HighResolutionTimerPrivate; + +class SCORE_LIB_BASE_EXPORT HighResolutionTimer : public QObject +{ + W_OBJECT(HighResolutionTimer) + +public: + explicit HighResolutionTimer(double frequencyHz, QObject* parent = nullptr); + ~HighResolutionTimer(); + + void start(); + void stop(); + + bool isRunning() const; + double frequency() const; + + void timeout(score::HighResolutionTimer* self) E_SIGNAL(SCORE_LIB_BASE_EXPORT, timeout, self); + void started(score::HighResolutionTimer* self) E_SIGNAL(SCORE_LIB_BASE_EXPORT, started, self); + void stopped(score::HighResolutionTimer* self) E_SIGNAL(SCORE_LIB_BASE_EXPORT, stopped, self); + + void setMaximumAccuracy(bool); +private: + void timerEvent(QTimerEvent *k) override; + std::unique_ptr d; +}; + +class SCORE_LIB_BASE_EXPORT Timers : public QObject +{ + W_OBJECT(Timers) + +public: + static Timers& instance(); + + explicit Timers(QObject* parent = nullptr); + ~Timers() override; + + HighResolutionTimer* acquireTimer(QObject* user, double frequencyHz); + void releaseTimer(QObject* user, HighResolutionTimer* timerId); + +private: + struct TimerEntry + { + std::unique_ptr timer; + ossia::small_flat_map users; + }; + + mutable std::mutex m_mutex; + ossia::small_flat_map m_timers; // frequency -> entry +}; +} + +W_REGISTER_ARGTYPE(score::HighResolutionTimer*) diff --git a/src/plugins/score-plugin-avnd/Crousti/GpuUtils.cpp b/src/plugins/score-plugin-avnd/Crousti/GpuUtils.cpp index f50201c6cb..08671078a7 100644 --- a/src/plugins/score-plugin-avnd/Crousti/GpuUtils.cpp +++ b/src/plugins/score-plugin-avnd/Crousti/GpuUtils.cpp @@ -70,9 +70,6 @@ void CustomGpuOutputNodeBase::startRendering() { } void CustomGpuOutputNodeBase::render() { - if(m_update) - m_update(); - auto renderer = m_renderer.lock(); if(renderer && m_renderState) { @@ -95,12 +92,9 @@ bool CustomGpuOutputNodeBase::canRender() const void CustomGpuOutputNodeBase::onRendererChange() { } -void CustomGpuOutputNodeBase::createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) +void CustomGpuOutputNodeBase::createOutput(score::gfx::OutputConfiguration conf) { - m_update = onUpdate; - onReady(); + conf.onReady(); } void CustomGpuOutputNodeBase::destroyOutput() { } diff --git a/src/plugins/score-plugin-avnd/Crousti/GpuUtils.hpp b/src/plugins/score-plugin-avnd/Crousti/GpuUtils.hpp index c980945047..485f7e1993 100644 --- a/src/plugins/score-plugin-avnd/Crousti/GpuUtils.hpp +++ b/src/plugins/score-plugin-avnd/Crousti/GpuUtils.hpp @@ -322,7 +322,6 @@ struct SCORE_PLUGIN_AVND_EXPORT CustomGpuOutputNodeBase const score::DocumentContext& m_ctx; std::weak_ptr m_renderer{}; std::shared_ptr m_renderState{}; - std::function m_update; QString vertex, fragment, compute; score::gfx::Message last_message; @@ -338,9 +337,7 @@ struct SCORE_PLUGIN_AVND_EXPORT CustomGpuOutputNodeBase bool canRender() const override; void onRendererChange() override; - void createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) override; + void createOutput(score::gfx::OutputConfiguration) override; void destroyOutput() override; std::shared_ptr renderState() const override; diff --git a/src/plugins/score-plugin-gfx/Gfx/Filter/PreviewWidget.cpp b/src/plugins/score-plugin-gfx/Gfx/Filter/PreviewWidget.cpp index f6c269aa43..6f132978e1 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Filter/PreviewWidget.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Filter/PreviewWidget.cpp @@ -257,7 +257,8 @@ class ShaderPreviewManager : public QObject ShaderPreviewManager() : QObject{qApp} { - m_screen = std::make_unique(true); + score::gfx::OutputNode::Configuration conf{}; + m_screen = std::make_unique(conf, true); m_graph.addNode(m_screen.get()); } @@ -448,9 +449,9 @@ class ShaderPreviewManager : public QObject } } + std::unique_ptr m_screen{}; private: std::unique_ptr m_isf{}; - std::unique_ptr m_screen{}; std::vector> m_textures; score::gfx::Graph m_graph{}; ProcessedProgram m_program; @@ -520,6 +521,7 @@ void ShaderPreviewWidget::timerEvent(QTimerEvent* event) if(g_shaderPreview) { g_shaderPreview->updateControls(); + g_shaderPreview->m_screen->render(); } } } diff --git a/src/plugins/score-plugin-gfx/Gfx/GfxContext.cpp b/src/plugins/score-plugin-gfx/Gfx/GfxContext.cpp index 36172863f7..fecdd610c3 100644 --- a/src/plugins/score-plugin-gfx/Gfx/GfxContext.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/GfxContext.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -14,9 +15,9 @@ #include #include +#include namespace Gfx { - GfxContext::GfxContext(const score::DocumentContext& ctx) : m_context{ctx} { @@ -37,16 +38,19 @@ GfxContext::GfxContext(const score::DocumentContext& ctx) m_graph = new score::gfx::Graph; double rate = m_context.app.settings().getRate(); - rate = 1000. / qBound(1.0, rate, 1000.); + rate = qBound(1.0, rate, 1000.); + - QMetaObject::invokeMethod(this, [this, rate] { - m_no_vsync_timer = startTimer(rate, Qt::PreciseTimer); - }, Qt::QueuedConnection); + { + m_no_vsync_timer = m_timers.acquireTimer(this, rate); + connect(m_no_vsync_timer, &score::HighResolutionTimer::timeout, this, &GfxContext::on_no_vsync_timer, Qt::UniqueConnection); + } // A safety timer necessary to handle graph updates in case we had vsync and lost it - QMetaObject::invokeMethod(this, [this] { - m_watchdog_timer = startTimer(50, Qt::PreciseTimer); - }, Qt::QueuedConnection); + { + m_watchdog_timer = m_timers.acquireTimer(this, 20.); + connect(m_watchdog_timer, &score::HighResolutionTimer::timeout, this, &GfxContext::on_watchdog_timer, Qt::UniqueConnection); + } } GfxContext::~GfxContext() @@ -168,36 +172,42 @@ void GfxContext::recompute_edges() { add_edge(edge); } + for(auto edge : preview_edges) + { + add_edge(edge); + } } void GfxContext::recompute_graph() { - if(m_no_vsync_timer != -1) - killTimer(m_no_vsync_timer); - for(auto [id, ptr] : m_manualTimers) - killTimer(id); + // Clear previous timers + std::destroy_at(&m_timers); + std::construct_at(&m_timers); + m_no_vsync_timer = nullptr; m_manualTimers.clear(); - m_no_vsync_timer = -1; - m_graph->setVSyncCallback({}); + for(auto& output : m_graph->outputs()) + { + output->setVSyncCallback({}); + } + // Recreate the graph recompute_edges(); auto& settings = m_context.app.settings(); + const double settings_rate = m_context.app.settings().getRate(); const auto api = settings.graphicsApiEnum(); m_graph->createAllRenderLists(api); + // Recreate new timers const bool vsync = settings.getVSync() && m_graph->canDoVSync(); - // rate in fps - double rate = m_context.app.settings().getRate(); - rate = 1000. / qBound(1.0, rate, 1000.); - // Update and render // This starts the timer for updating the graph, that is, reading the new parameters. if(vsync) { + // Only one thread / renderer, best case, we use it for updating the graph, reading the commands etc. #if defined(SCORE_THREADED_GFX) if(api == Vulkan) { @@ -206,24 +216,56 @@ void GfxContext::recompute_graph() m_thread.start(); } #endif - m_graph->setVSyncCallback([this] { updateGraph(); }); + SCORE_ASSERT(m_graph->outputs().size() == 1); + SCORE_ASSERT(m_graph->outputs()[0]); + m_graph->outputs().front()->setVSyncCallback([this] { updateGraph(); }); } else { - QMetaObject::invokeMethod(this, [this, rate] { - m_no_vsync_timer = startTimer(rate, Qt::PreciseTimer); - }, Qt::QueuedConnection); - } + // Multiple renderers, so we have one timer at the highest fps for updating the graph before anything gets rendered. - // This starts the timers which control the actual render rate of various things + // rate in fps + double rate = settings_rate; - for(auto& outputs : m_graph->renderLists()) - { - auto conf = outputs->output.configuration(); - if(conf.manualRenderingRate) + for(auto& output : m_graph->outputs()) { - int id = startTimer(*conf.manualRenderingRate, Qt::PreciseTimer); - m_manualTimers[id] = &outputs->output; + auto conf = output->configuration(); + if(conf.manualRenderingRate) + { + rate = std::max(1000. / *conf.manualRenderingRate, rate); + } + } + + rate = qBound(1.0, rate, 1000.); + + m_no_vsync_timer = m_timers.acquireTimer(this, rate); + connect(m_no_vsync_timer, &score::HighResolutionTimer::timeout, this, &GfxContext::on_no_vsync_timer, Qt::ConnectionType(Qt::UniqueConnection|Qt::QueuedConnection)); + + + // This starts the timers which control the actual render rate of various things + for(auto& output : m_graph->outputs()) + { + auto conf = output->configuration(); + if(conf.manualRenderingRate) + { + bool existing_timer{}; + for(auto& tm : m_manualTimers) + { + if(tm.first->frequency() == 1000. / *conf.manualRenderingRate) + { + tm.second.insert(output); + existing_timer = true; + break; + } + } + + if(!existing_timer) + { + auto id = m_timers.acquireTimer(this, 1000. / *conf.manualRenderingRate); + m_manualTimers[id].insert(output); + connect(id, &score::HighResolutionTimer::timeout, this, &GfxContext::on_manual_timer, Qt::QueuedConnection); + } + } } } } @@ -237,10 +279,14 @@ void GfxContext::add_preview_output(score::gfx::OutputNode& node) // rate in fps double rate = m_context.app.settings().getRate(); - rate = 1000. / qBound(1.0, rate, 1000.); - if(m_no_vsync_timer == -1) - m_no_vsync_timer = startTimer(rate, Qt::PreciseTimer); + // Timer for graph update + if(m_no_vsync_timer == nullptr) { + m_no_vsync_timer = m_timers.acquireTimer(this, rate); + connect(m_no_vsync_timer, &score::HighResolutionTimer::timeout, this, &GfxContext::on_no_vsync_timer, Qt::UniqueConnection); + } + + // Render is done in the widget } void GfxContext::recompute_connections() @@ -294,10 +340,18 @@ void GfxContext::remove_node( // Remove the node from the timers if it's in there for(auto timer_it = m_manualTimers.begin(); timer_it != m_manualTimers.end();) { - if(timer_it->second == node) + auto& nodes = timer_it->second; + nodes.erase((score::gfx::OutputNode*)node); + + if(nodes.empty()) + { + m_timers.releaseTimer(this, timer_it->first); timer_it = m_manualTimers.erase(timer_it); + } else + { ++timer_it; + } } m_graph->removeNode(node); @@ -385,15 +439,12 @@ void GfxContext::run_commands() } } + for(auto* out : add_output) + add_preview_output(*safe_cast(out)); if(recompute) { recompute_graph(); } - else - { - for(auto* out : add_output) - add_preview_output(*safe_cast(out)); - } // This will force the nodes to be deleted in the main thread a bit later // as for some reason when the ScreenNode is deleted, it still gets rendered to... @@ -422,18 +473,23 @@ void GfxContext::updateGraph() } } -void GfxContext::timerEvent(QTimerEvent* ev) +void GfxContext::on_no_vsync_timer(score::HighResolutionTimer* self) { - const auto tid = ev->timerId(); - if(tid == m_no_vsync_timer || tid == m_watchdog_timer) - { + updateGraph(); +} + +void GfxContext::on_watchdog_timer(score::HighResolutionTimer* self) +{ + if(m_manualTimers.empty()) updateGraph(); - } - else +} + +void GfxContext::on_manual_timer(score::HighResolutionTimer* self) +{ + if(auto ptr = m_manualTimers.find(self); ptr != m_manualTimers.end()) { - if(auto ptr = m_manualTimers.find(tid); ptr != m_manualTimers.end()) - { - ptr->second->render(); + for(auto output : ptr->second) { + output->render(); } } } diff --git a/src/plugins/score-plugin-gfx/Gfx/GfxContext.hpp b/src/plugins/score-plugin-gfx/Gfx/GfxContext.hpp index 1dec223d3b..b401d74baf 100644 --- a/src/plugins/score-plugin-gfx/Gfx/GfxContext.hpp +++ b/src/plugins/score-plugin-gfx/Gfx/GfxContext.hpp @@ -8,11 +8,16 @@ #include #include #include +#include #include #include - +#include #include #include +namespace score { +class HighResolutionTimer; +class Timers; +} namespace score::gfx { struct Graph; @@ -65,7 +70,9 @@ class SCORE_PLUGIN_GFX_EXPORT GfxContext : public QObject void remove_edge(EdgeSpec e); void remove_node(std::vector>& nursery, int32_t id); - void timerEvent(QTimerEvent*) override; + void on_no_vsync_timer(score::HighResolutionTimer* self); + void on_watchdog_timer(score::HighResolutionTimer* self); + void on_manual_timer(score::HighResolutionTimer* self); const score::DocumentContext& m_context; std::atomic_int32_t index{1}; ossia::hash_map nodes; @@ -107,12 +114,14 @@ class SCORE_PLUGIN_GFX_EXPORT GfxContext : public QObject ossia::flat_set preview_edges; std::atomic_bool edges_changed{}; - int m_no_vsync_timer{-1}; - int m_watchdog_timer{-1}; + score::HighResolutionTimer* m_no_vsync_timer{}; + score::HighResolutionTimer* m_watchdog_timer{}; - ossia::flat_map m_manualTimers; + ossia::small_flat_map, 8> m_manualTimers; ossia::object_pool> m_buffers; + + score::Timers m_timers; }; } diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/Graph.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/Graph.cpp index b2f5d5a4c5..d0e678b8ad 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/Graph.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/Graph.cpp @@ -251,20 +251,8 @@ void Graph::initializeOutput(OutputNode* output, GraphicsApi graphicsApi) recreateOutputRenderList(*output); }; - auto onUpdate = [this] { - switch(this->m_outputs.size()) - { - case 1: - if(this->m_vsync_callback) - this->m_vsync_callback(); - break; - default: - break; - } - }; - // TODO only works for one output !! - output->createOutput(graphicsApi, onReady, onUpdate, onResize); + output->createOutput({.graphicsApi = graphicsApi, .onReady = onReady, .onResize = onResize}); } else { @@ -369,18 +357,10 @@ void Graph::relinkGraph() } } -void Graph::setVSyncCallback(std::function cb) -{ - // TODO thread safety if vulkan uses a thread ? - // If we have more than one output, then instead we sync them with - // a simple timer, as they may have drastically different vsync rates. - m_vsync_callback = cb; -} - bool Graph::canDoVSync() const noexcept { return m_outputs.size() == 1 - && !m_outputs[0]->configuration().manualRenderingRate.has_value(); + && m_outputs[0]->configuration().supportsVSync; } static bool createNodeRenderer(score::gfx::Node& node, RenderList& r) @@ -409,9 +389,36 @@ Graph::createRenderList(OutputNode* output, std::shared_ptr state) output->setRenderer(ptr); for(auto& node : m_nodes) node->addedToGraph = false; +#if 0 + for(auto& model : m_nodes) + qDebug() << "Model: " << typeid(*model).name(); + for(auto node : m_nodes) + { + qDebug() << node->nodeId << typeid(*node).name(); + for(auto inlet : node->input) + { + qDebug() << "Inlet: " << magic_enum::enum_name(inlet->type) << inlet->edges.size(); + for(auto edge : inlet->edges) { + qDebug() << edge->source->node << " => "<< edge->sink->node; + } + } + for(auto outlet : node->output) + { + qDebug() << "Outlet: " << magic_enum::enum_name(outlet->type) << outlet->edges.size(); + for(auto edge : outlet->edges) { + qDebug() << edge->source->node << " => "<< edge->sink->node; + } + } + } + for(auto edge : m_edges) + { + qDebug() << "Edge:" << edge->source->node << " => "<< edge->sink->node; + } +#endif RenderList& r = *ptr; auto& model_nodes = r.nodes; + { model_nodes.push_back(output); diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/Graph.hpp b/src/plugins/score-plugin-gfx/Gfx/Graph/Graph.hpp index a683e49b35..33f88cbe69 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/Graph.hpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/Graph.hpp @@ -76,14 +76,6 @@ struct SCORE_PLUGIN_GFX_EXPORT Graph */ void relinkGraph(); - /** - * @brief Set a callback to drive rendering when there is a single output. - * - * If we render to a single window, we can use the GPU V-Sync mechanism. - * Otherwise the implementation will create timers to keep things in sync. - */ - void setVSyncCallback(std::function); - /** * @brief True if the graph supports being driven by the screen vertical synchronization. */ @@ -94,6 +86,11 @@ struct SCORE_PLUGIN_GFX_EXPORT Graph return m_renderers; } + std::span outputs() const noexcept + { + return m_outputs; + } + private: void initializeOutput(OutputNode* output, GraphicsApi graphicsApi); void createOutputRenderList(OutputNode& output); @@ -103,7 +100,6 @@ struct SCORE_PLUGIN_GFX_EXPORT Graph std::vector> m_renderers; std::vector> m_unused_windows; - std::function m_vsync_callback; std::vector m_nodes; std::vector m_edges; diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/OutputNode.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/OutputNode.cpp index 7a689184ae..7275300449 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/OutputNode.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/OutputNode.cpp @@ -7,7 +7,7 @@ OutputNode::OutputNode() { } OutputNode::~OutputNode() { } void OutputNode::updateGraphicsAPI(GraphicsApi) { } - +void OutputNode::setVSyncCallback(std::function) { } OutputNodeRenderer::~OutputNodeRenderer() { } void OutputNodeRenderer::finishFrame( diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/OutputNode.hpp b/src/plugins/score-plugin-gfx/Gfx/Graph/OutputNode.hpp index 2dd0e745c8..5618ae07d7 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/OutputNode.hpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/OutputNode.hpp @@ -7,6 +7,13 @@ #include namespace score::gfx { +struct OutputConfiguration +{ + GraphicsApi graphicsApi{}; + std::function onReady; + std::function onResize; +}; + class SCORE_PLUGIN_GFX_EXPORT OutputNodeRenderer : public score::gfx::NodeRenderer { public: @@ -36,10 +43,15 @@ class SCORE_PLUGIN_GFX_EXPORT OutputNode : public score::gfx::Node virtual bool canRender() const = 0; virtual void onRendererChange() = 0; - virtual void createOutput( - GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) - = 0; + /** + * @brief Set a callback to drive rendering when there is a single output. + * + * If we render to a single window, we can use the GPU V-Sync mechanism. + * Otherwise the implementation will create timers to keep things in sync. + */ + virtual void setVSyncCallback(std::function); + + virtual void createOutput(OutputConfiguration conf) = 0; virtual void updateGraphicsAPI(GraphicsApi); virtual void destroyOutput() = 0; @@ -51,6 +63,7 @@ class SCORE_PLUGIN_GFX_EXPORT OutputNode : public score::gfx::Node // rate (given in milliseconds) std::optional manualRenderingRate; bool outputNeedsRenderPass{}; + bool supportsVSync{}; OutputNode* parent{}; }; diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/PreviewNode.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/PreviewNode.cpp index d423a3b6c8..1d4f95ff3e 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/PreviewNode.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/PreviewNode.cpp @@ -64,9 +64,6 @@ void PreviewNode::startRendering() { } void PreviewNode::render() { - if(m_update) - m_update(); - auto renderer = m_renderer.lock(); if(renderer && m_renderState) { @@ -99,17 +96,14 @@ score::gfx::RenderList* PreviewNode::renderer() const return m_renderer.lock().get(); } -void PreviewNode::createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) +void PreviewNode::createOutput(score::gfx::OutputConfiguration conf) { m_renderState = std::make_shared(); - m_update = onUpdate; m_renderState = importRenderState(QSize(m_settings.width, m_settings.height), m_rhi); m_renderState->renderPassDescriptor = m_renderTarget->renderPassDescriptor(); - onReady(); + conf.onReady(); } void PreviewNode::destroyOutput() { } diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/PreviewNode.hpp b/src/plugins/score-plugin-gfx/Gfx/Graph/PreviewNode.hpp index 06b451c283..79d3e5f860 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/PreviewNode.hpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/PreviewNode.hpp @@ -22,9 +22,7 @@ class SCORE_PLUGIN_GFX_EXPORT PreviewNode : public score::gfx::OutputNode void setRenderer(std::shared_ptr r) override; score::gfx::RenderList* renderer() const override; - void createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) override; + void createOutput(score::gfx::OutputConfiguration) override; void destroyOutput() override; std::shared_ptr renderState() const override; @@ -41,7 +39,6 @@ class SCORE_PLUGIN_GFX_EXPORT PreviewNode : public score::gfx::OutputNode std::weak_ptr m_renderer{}; QRhiRenderTarget* m_renderTarget{}; QRhiTexture* m_texture{}; - std::function m_update; std::shared_ptr m_renderState{}; }; } diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.cpp index b374d38660..8c9328f0e0 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.cpp @@ -88,7 +88,6 @@ createRenderState(GraphicsApi graphicsApi, QSize sz, QWindow* window) if(graphicsApi == Vulkan) { QRhiVulkanInitParams params; - params.deviceExtensions = QRhiVulkanInitParams::preferredInstanceExtensions(); #if defined(_WIN32) params.deviceExtensions << VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME << VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME @@ -184,8 +183,9 @@ createRenderState(QWindow& window, GraphicsApi graphicsApi) return createRenderState(graphicsApi, window.size(), &window); } -ScreenNode::ScreenNode(bool embedded, bool fullScreen) +ScreenNode::ScreenNode(Configuration conf, bool embedded, bool fullScreen) : OutputNode{} + , m_conf{conf} , m_embedded{embedded} , m_fullScreen{fullScreen} , m_ownsWindow{true} @@ -232,6 +232,8 @@ bool ScreenNode::canRender() const void ScreenNode::startRendering() { + if(onFps) + onFps(0.f); if(m_window) { m_window->onRender = [this](QRhiCommandBuffer& commands) { @@ -244,21 +246,30 @@ void ScreenNode::startRendering() } } -void ScreenNode::render() { } +void ScreenNode::render() +{ + // Used when we don't have vsync: request an update on the Window + if(m_window) + { + onRendererChange(); + m_window->render(); + } +} void ScreenNode::onRendererChange() { if(m_window) { - if(auto r = m_window->state->renderer.lock()) - { - m_window->m_canRender = r->renderers.size() > 1; - } - else + if(m_window->state) { - m_window->m_canRender = false; + if(auto r = m_window->state->renderer.lock()) + { + m_window->m_canRender = r->renderers.size() > 1; + return; + } } } + m_window->m_canRender = false; } void ScreenNode::stopRendering() @@ -271,6 +282,8 @@ void ScreenNode::stopRendering() m_window->state->renderer = {}; ////window->state->hasSwapChain = false; } + if(onFps) + onFps(0.f); } void ScreenNode::setRenderer(std::shared_ptr r) @@ -313,6 +326,11 @@ void ScreenNode::setTitle(QString title) } } +void ScreenNode::setConfiguration(Configuration conf) +{ + m_conf = conf; +} + void ScreenNode::setSize(QSize sz) { m_sz = sz; @@ -366,13 +384,11 @@ void ScreenNode::setCursor(bool b) } } -void ScreenNode::createOutput( - GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) +void ScreenNode::createOutput(score::gfx::OutputConfiguration conf) { if(m_ownsWindow) { - m_window = std::make_shared(graphicsApi); + m_window = std::make_shared(conf.graphicsApi); if(m_embedded) m_window->unsetCursor(); } @@ -401,8 +417,12 @@ void ScreenNode::createOutput( if(onKeyRelease) onKeyRelease(k, t); }); - m_window->onUpdate = std::move(onUpdate); - m_window->onWindowReady = [this, graphicsApi, onReady = std::move(onReady)] { + QObject::connect(m_window.get(), &Window::fps, [this](float f) { + if(onFps) + onFps(f); + }); + m_window->onUpdate = this->m_vsyncCallback; + m_window->onWindowReady = [this, graphicsApi=conf.graphicsApi, onReady = std::move(conf.onReady)] { m_window->state = createRenderState(*m_window, graphicsApi); m_window->state->window = m_window; m_window->state->renderSize = QSize(1280, 720); @@ -433,7 +453,7 @@ void ScreenNode::createOutput( onReady(); } }; - m_window->onResize = [this, onResize = std::move(onResize)] { + m_window->onResize = [this, onResize = std::move(conf.onResize)] { if(m_window && m_window->state) { auto& st = *m_window->state; @@ -550,6 +570,16 @@ void ScreenNode::updateGraphicsAPI(GraphicsApi api) } } +void ScreenNode::setVSyncCallback(std::function f) +{ + // TODO thread safety if vulkan uses a thread ? + // If we have more than one output, then instead we sync them with + // a simple timer, as they may have drastically different vsync rates. + m_vsyncCallback = f; + if(m_window) + m_window->onUpdate = m_vsyncCallback; +} + std::shared_ptr ScreenNode::renderState() const { if(m_window && m_window->m_swapChain) @@ -569,7 +599,7 @@ score::gfx::OutputNodeRenderer* ScreenNode::createRenderer(RenderList& r) const OutputNode::Configuration ScreenNode::configuration() const noexcept { - return {}; + return m_conf; } diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.hpp b/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.hpp index fd28820463..eacddc72f9 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.hpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/ScreenNode.hpp @@ -10,7 +10,7 @@ namespace score::gfx */ struct SCORE_PLUGIN_GFX_EXPORT ScreenNode : OutputNode { - explicit ScreenNode(bool embedded = false, bool startFullScreen = false); + explicit ScreenNode(Configuration conf, bool embedded = false, bool startFullScreen = false); virtual ~ScreenNode(); void startRendering() override; @@ -29,12 +29,12 @@ struct SCORE_PLUGIN_GFX_EXPORT ScreenNode : OutputNode void setFullScreen(bool); void setCursor(bool); void setTitle(QString); + void setConfiguration(Configuration); - void createOutput( - GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) override; + void createOutput(score::gfx::OutputConfiguration) override; void destroyOutput() override; void updateGraphicsAPI(GraphicsApi) override; + void setVSyncCallback(std::function) override; std::shared_ptr renderState() const override; score::gfx::OutputNodeRenderer* createRenderer(RenderList& r) const noexcept override; @@ -47,8 +47,10 @@ struct SCORE_PLUGIN_GFX_EXPORT ScreenNode : OutputNode std::function onTabletMove; std::function onKey; std::function onKeyRelease; + std::function onFps; private: + Configuration m_conf; std::shared_ptr m_window{}; QRhiSwapChain* m_swapChain{}; QRhiRenderBuffer* m_depthStencil{}; @@ -57,6 +59,7 @@ struct SCORE_PLUGIN_GFX_EXPORT ScreenNode : OutputNode std::optional m_pos{}; std::optional m_sz{}; std::optional m_renderSz{}; + std::function m_vsyncCallback; bool m_embedded{}; bool m_fullScreen{}; diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/Window.cpp b/src/plugins/score-plugin-gfx/Gfx/Graph/Window.cpp index 951efbab6e..a2e7de5e4c 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/Window.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/Window.cpp @@ -127,6 +127,7 @@ void Window::releaseSwapChain() void Window::render() { + static constexpr double fps_smoothing = .8; if(m_closed) return; @@ -176,6 +177,20 @@ void Window::render() onRender(*commands); state->rhi->endFrame(m_swapChain, {}); + { + // 1. Calculate the time elapsed since the last frame + if(const auto frame_ns = m_timer.nsecsElapsed(); frame_ns > 0) + { + const double fps = 1e9 / frame_ns; + + // 2. Smooth things a bit + if(m_fps == 0.0f) + m_fps = fps; + else + m_fps = (fps * fps_smoothing) + (m_fps * (1.0f - fps_smoothing)); + } + m_timer.restart(); + } } else { @@ -202,8 +217,15 @@ void Window::render() buf->endPass(); state->rhi->endFrame(m_swapChain, {}); + m_fps = 0.; + } + + fps(m_fps); + + if(this->onUpdate) { + // requestUpdate is only to be used in the vsync case + requestUpdate(); } - requestUpdate(); } void Window::exposeEvent(QExposeEvent* ev) diff --git a/src/plugins/score-plugin-gfx/Gfx/Graph/Window.hpp b/src/plugins/score-plugin-gfx/Gfx/Graph/Window.hpp index 77d202db47..cd4c6b0815 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Graph/Window.hpp +++ b/src/plugins/score-plugin-gfx/Gfx/Graph/Window.hpp @@ -2,6 +2,7 @@ #include +#include #include #include @@ -54,10 +55,14 @@ class SCORE_PLUGIN_GFX_EXPORT Window : public QWindow void key(int key, const QString& t) W_SIGNAL(key, key, t); void keyRelease(int key, const QString& t) W_SIGNAL(keyRelease, key, t); + void fps(float v) W_SIGNAL(fps, v); + private: std::shared_ptr state; GraphicsApi m_api{}; QRhiSwapChain* m_swapChain{}; + QElapsedTimer m_timer; + double m_fps{0.}; bool m_closed = false; bool m_canRender = false; diff --git a/src/plugins/score-plugin-gfx/Gfx/Libav/LibavEncoderNode.cpp b/src/plugins/score-plugin-gfx/Gfx/Libav/LibavEncoderNode.cpp index ff699f941f..5df0636dc5 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Libav/LibavEncoderNode.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Libav/LibavEncoderNode.cpp @@ -35,8 +35,6 @@ void LibavEncoderNode::render() { if(!encoder.available()) return; - if(m_update) - m_update(); auto renderer = m_renderer.lock(); if(renderer && m_renderState) @@ -79,12 +77,9 @@ score::gfx::RenderList* LibavEncoderNode::renderer() const return m_renderer.lock().get(); } -void LibavEncoderNode::createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) +void LibavEncoderNode::createOutput(score::gfx::OutputConfiguration conf) { m_renderState = std::make_shared(); - m_update = onUpdate; m_renderState->surface = QRhiGles2InitParams::newFallbackSurface(); QRhiGles2InitParams params; @@ -108,7 +103,7 @@ void LibavEncoderNode::createOutput( m_renderTarget->setRenderPassDescriptor(m_renderState->renderPassDescriptor); m_renderTarget->create(); - onReady(); + conf.onReady(); } void LibavEncoderNode::destroyOutput() { } diff --git a/src/plugins/score-plugin-gfx/Gfx/Libav/LibavEncoderNode.hpp b/src/plugins/score-plugin-gfx/Gfx/Libav/LibavEncoderNode.hpp index dda5c96476..c409860354 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Libav/LibavEncoderNode.hpp +++ b/src/plugins/score-plugin-gfx/Gfx/Libav/LibavEncoderNode.hpp @@ -17,7 +17,6 @@ struct LibavEncoderNode : score::gfx::OutputNode std::weak_ptr m_renderer{}; QRhiTexture* m_texture{}; QRhiTextureRenderTarget* m_renderTarget{}; - std::function m_update; std::shared_ptr m_renderState{}; bool m_hasSender{}; @@ -30,9 +29,7 @@ struct LibavEncoderNode : score::gfx::OutputNode void setRenderer(std::shared_ptr r) override; score::gfx::RenderList* renderer() const override; - void createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) override; + void createOutput(score::gfx::OutputConfiguration) override; void destroyOutput() override; std::shared_ptr renderState() const override; diff --git a/src/plugins/score-plugin-gfx/Gfx/Sh4lt/Sh4ltOutputDevice.cpp b/src/plugins/score-plugin-gfx/Gfx/Sh4lt/Sh4ltOutputDevice.cpp index a5387e5242..663b8db9be 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Sh4lt/Sh4ltOutputDevice.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Sh4lt/Sh4ltOutputDevice.cpp @@ -67,7 +67,6 @@ struct Sh4ltOutputNode : score::gfx::OutputNode std::weak_ptr m_renderer{}; QRhiTexture* m_texture{}; QRhiTextureRenderTarget* m_renderTarget{}; - std::function m_update; std::shared_ptr m_renderState{}; std::shared_ptr m_writer{}; int64_t m_frame_counter{0}; @@ -83,9 +82,7 @@ struct Sh4ltOutputNode : score::gfx::OutputNode void setRenderer(std::shared_ptr r) override; score::gfx::RenderList* renderer() const override; - void createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) override; + void createOutput(score::gfx::OutputConfiguration conf) override; void destroyOutput() override; std::shared_ptr renderState() const override; @@ -140,9 +137,6 @@ void Sh4ltOutputNode::startRendering() { } void Sh4ltOutputNode::render() { - if(m_update) - m_update(); - auto renderer = m_renderer.lock(); if(renderer && m_renderState) { @@ -184,9 +178,7 @@ score::gfx::RenderList* Sh4ltOutputNode::renderer() const return m_renderer.lock().get(); } -void Sh4ltOutputNode::createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) +void Sh4ltOutputNode::createOutput(score::gfx::OutputConfiguration conf) { m_writer = std::make_unique( sh4lt::shtype::shtype_from_gst_caps( @@ -198,7 +190,6 @@ void Sh4ltOutputNode::createOutput( m_settings.width * m_settings.height * 4, m_logger); m_frame_dur = 1e9 / m_settings.rate; m_renderState = std::make_shared(); - m_update = onUpdate; m_renderState->surface = QRhiGles2InitParams::newFallbackSurface(); QRhiGles2InitParams params; @@ -222,7 +213,7 @@ void Sh4ltOutputNode::createOutput( m_renderTarget->setRenderPassDescriptor(m_renderState->renderPassDescriptor); m_renderTarget->create(); - onReady(); + conf.onReady(); } void Sh4ltOutputNode::destroyOutput() diff --git a/src/plugins/score-plugin-gfx/Gfx/Shmdata/ShmdataOutputDevice.cpp b/src/plugins/score-plugin-gfx/Gfx/Shmdata/ShmdataOutputDevice.cpp index f7e86993ea..237351f002 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Shmdata/ShmdataOutputDevice.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Shmdata/ShmdataOutputDevice.cpp @@ -66,7 +66,6 @@ struct ShmdataOutputNode : score::gfx::OutputNode std::weak_ptr m_renderer{}; QRhiTexture* m_texture{}; QRhiTextureRenderTarget* m_renderTarget{}; - std::function m_update; std::shared_ptr m_renderState{}; std::shared_ptr m_writer{}; bool m_hasSender{}; @@ -80,9 +79,7 @@ struct ShmdataOutputNode : score::gfx::OutputNode void setRenderer(std::shared_ptr r) override; score::gfx::RenderList* renderer() const override; - void createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) override; + void createOutput(score::gfx::OutputConfiguration conf) override; void destroyOutput() override; std::shared_ptr renderState() const override; @@ -136,9 +133,6 @@ void ShmdataOutputNode::startRendering() { } void ShmdataOutputNode::render() { - if(m_update) - m_update(); - auto renderer = m_renderer.lock(); if(renderer && m_renderState) { @@ -176,9 +170,7 @@ score::gfx::RenderList* ShmdataOutputNode::renderer() const return m_renderer.lock().get(); } -void ShmdataOutputNode::createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) +void ShmdataOutputNode::createOutput(score::gfx::OutputConfiguration conf) { // clang-format off m_writer = std::make_unique( @@ -189,7 +181,6 @@ void ShmdataOutputNode::createOutput( &m_logger); // clang-format on m_renderState = std::make_shared(); - m_update = onUpdate; m_renderState->surface = QRhiGles2InitParams::newFallbackSurface(); QRhiGles2InitParams params; @@ -213,7 +204,7 @@ void ShmdataOutputNode::createOutput( m_renderTarget->setRenderPassDescriptor(m_renderState->renderPassDescriptor); m_renderTarget->create(); - onReady(); + conf.onReady(); } void ShmdataOutputNode::destroyOutput() diff --git a/src/plugins/score-plugin-gfx/Gfx/Spout/SpoutOutput.cpp b/src/plugins/score-plugin-gfx/Gfx/Spout/SpoutOutput.cpp index 83ef37c0ff..ae0e2d7945 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Spout/SpoutOutput.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/Spout/SpoutOutput.cpp @@ -276,9 +276,6 @@ struct SpoutNode final : score::gfx::OutputNode void render() override { - if(m_update) - m_update(); - auto renderer = m_renderer.lock(); if(renderer && m_renderState) { @@ -534,15 +531,12 @@ struct SpoutNode final : score::gfx::OutputNode score::gfx::RenderList* renderer() const override { return m_renderer.lock().get(); } - void createOutput( - score::gfx::GraphicsApi graphicsApi, std::function onReady, - std::function onUpdate, std::function onResize) override + void createOutput(score::gfx::OutputConfiguration conf) override { m_renderState = std::make_shared(); - m_update = onUpdate; // Choose backend based on requested API - switch(graphicsApi) + switch(conf.graphicsApi) { case score::gfx::GraphicsApi::D3D11: createOutputD3D11(); @@ -583,7 +577,8 @@ struct SpoutNode final : score::gfx::OutputNode m_renderTarget->setRenderPassDescriptor(m_renderState->renderPassDescriptor); m_renderTarget->create(); - onReady(); + if(conf.onReady) + conf.onReady(); } void createOutputOpenGL() @@ -670,7 +665,6 @@ struct SpoutNode final : score::gfx::OutputNode params.inst = vkInst; // Enable required device extensions for external memory - params.deviceExtensions = QRhiVulkanInitParams::preferredInstanceExtensions(); params.deviceExtensions << VK_KHR_EXTERNAL_MEMORY_EXTENSION_NAME << VK_KHR_EXTERNAL_MEMORY_WIN32_EXTENSION_NAME << VK_KHR_DEDICATED_ALLOCATION_EXTENSION_NAME @@ -1044,7 +1038,6 @@ struct SpoutNode final : score::gfx::OutputNode std::weak_ptr m_renderer{}; QRhiTexture* m_texture{}; QRhiTextureRenderTarget* m_renderTarget{}; - std::function m_update; std::shared_ptr m_renderState{}; // OpenGL backend diff --git a/src/plugins/score-plugin-gfx/Gfx/Syphon/SyphonOutput.mm b/src/plugins/score-plugin-gfx/Gfx/Syphon/SyphonOutput.mm index dbfc5b22c1..b6073fa78e 100644 --- a/src/plugins/score-plugin-gfx/Gfx/Syphon/SyphonOutput.mm +++ b/src/plugins/score-plugin-gfx/Gfx/Syphon/SyphonOutput.mm @@ -112,9 +112,6 @@ void render() override if(!m_created) return; - if (m_update) - m_update(); - auto renderer = m_renderer.lock(); if (renderer && renderer->nodes.size() > 1 && m_renderState) { @@ -178,18 +175,13 @@ void setRenderer(std::shared_ptr r) override return m_renderer.lock().get(); } - void createOutput( - score::gfx::GraphicsApi graphicsApi, - std::function onReady, - std::function onUpdate, - std::function onResize) override + void createOutput(score::gfx::OutputConfiguration conf) override { m_renderState = std::make_shared(); - m_update = onUpdate; m_renderState->renderSize = QSize(m_settings.width, m_settings.height); m_renderState->outputSize = m_renderState->renderSize; - if (graphicsApi == score::gfx::GraphicsApi::Metal) + if (conf.graphicsApi == score::gfx::GraphicsApi::Metal) { // Metal backend QRhiMetalInitParams params; @@ -200,7 +192,7 @@ void createOutput( } else { - // OpenGL backend (default) + // OpenGL backend m_renderState->surface = QRhiGles2InitParams::newFallbackSurface(); QRhiGles2InitParams params; params.format.setMajorVersion(3); @@ -230,7 +222,7 @@ void createOutput( } createSyphon(*rhi); - onReady(); + conf.onReady(); } void destroyOutput() override @@ -293,7 +285,6 @@ Configuration configuration() const noexcept override{ std::weak_ptr m_renderer{}; QRhiTexture* m_texture{}; QRhiTextureRenderTarget* m_renderTarget{}; - std::function m_update; std::shared_ptr m_renderState{}; // OpenGL Syphon server diff --git a/src/plugins/score-plugin-gfx/Gfx/TexturePort.cpp b/src/plugins/score-plugin-gfx/Gfx/TexturePort.cpp index 156e1d377b..ead5dcb7fc 100644 --- a/src/plugins/score-plugin-gfx/Gfx/TexturePort.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/TexturePort.cpp @@ -44,7 +44,9 @@ class GraphPreviewWidget : public QWidget , plug{&plug} { setLayout(new Inspector::VBoxLayout{this}); - auto window = std::make_unique(true); + + score::gfx::OutputNode::Configuration conf{}; + auto window = std::make_unique(conf, true); node = window.get(); screenId = plug.context.register_preview_node(std::move(window)); if(screenId != -1) @@ -101,6 +103,7 @@ class GraphPreviewWidget : public QWidget container->setMaximumHeight(200); this->layout()->addWidget(container); } + node->render(); } ~GraphPreviewWidget() diff --git a/src/plugins/score-plugin-gfx/Gfx/WindowDevice.cpp b/src/plugins/score-plugin-gfx/Gfx/WindowDevice.cpp index 46a1c60bd1..8e2fcc52de 100644 --- a/src/plugins/score-plugin-gfx/Gfx/WindowDevice.cpp +++ b/src/plugins/score-plugin-gfx/Gfx/WindowDevice.cpp @@ -9,6 +9,7 @@ #include +#include #include #include #include @@ -31,7 +32,26 @@ namespace Gfx static score::gfx::ScreenNode* createScreenNode() { const auto& settings = score::AppContext().applicationSettings; - return new score::gfx::ScreenNode{false, (settings.autoplay || !settings.gui)}; + const auto& gfx_settings = score::AppContext().settings(); + + auto make_configuration = [&] { + score::gfx::OutputNode::Configuration conf; + double rate = gfx_settings.getRate(); + if(rate > 0) + conf = {.manualRenderingRate = 1000. / rate, .supportsVSync = true}; + else + conf = {.manualRenderingRate = {}, .supportsVSync = true}; + return conf; + }; + + auto node = new score::gfx::ScreenNode{ + make_configuration(), false, (settings.autoplay || !settings.gui)}; + + QObject::connect( + &gfx_settings, &Gfx::Settings::Model::RateChanged, node, + [node, make_configuration] { node->setConfiguration(make_configuration()); }); + + return node; } class window_device : public ossia::net::device_base @@ -397,6 +417,13 @@ class window_device : public ossia::net::device_base }); m_root.add_child(std::move(fs_node)); } + + { + auto fps_node = std::make_unique("fps", *this, m_root); + auto fps_param = fps_node->create_parameter(ossia::val_type::FLOAT); + m_screen->onFps = [fps_param](float fps) { fps_param->push_value(fps); }; + m_root.add_child(std::move(fps_node)); + } } const gfx_node_base& get_root_node() const override { return m_root; } diff --git a/src/plugins/score-plugin-protocols/Protocols/MCU/MCUProtocolFactory.cpp b/src/plugins/score-plugin-protocols/Protocols/MCU/MCUProtocolFactory.cpp index 1587aa9d8a..3f44ee4826 100644 --- a/src/plugins/score-plugin-protocols/Protocols/MCU/MCUProtocolFactory.cpp +++ b/src/plugins/score-plugin-protocols/Protocols/MCU/MCUProtocolFactory.cpp @@ -109,7 +109,9 @@ bool MCUProtocolFactory::checkCompatibility( if(specif.output_handle.empty()) return false; // FIXME improve when we have multiple devices in one control surface - return specif.input_handle[0] != libremidi::port_information{} - && specif.output_handle[0] != libremidi::port_information{}; + return + specif.input_handle[0].port != libremidi::port_information{}.port + && specif.output_handle[0].port != libremidi::port_information{}.port + ; } } diff --git a/src/plugins/score-plugin-protocols/Protocols/MIDI/MIDIProtocolFactory.cpp b/src/plugins/score-plugin-protocols/Protocols/MIDI/MIDIProtocolFactory.cpp index 7449a0d601..83ce9e884d 100644 --- a/src/plugins/score-plugin-protocols/Protocols/MIDI/MIDIProtocolFactory.cpp +++ b/src/plugins/score-plugin-protocols/Protocols/MIDI/MIDIProtocolFactory.cpp @@ -236,7 +236,7 @@ bool MIDIInputProtocolFactory::checkCompatibility( { // FIXME check if we can open the same device multiple times ? auto specif = a.deviceSpecificSettings.value(); - return specif.handle != libremidi::port_information{} || specif.virtualPort; + return specif.handle.port != libremidi::port_information{}.port || specif.virtualPort; } QString MIDIOutputProtocolFactory::prettyName() const noexcept @@ -329,6 +329,6 @@ bool MIDIOutputProtocolFactory::checkCompatibility( { // FIXME check if we can open the same device multiple times ? auto specif = a.deviceSpecificSettings.value(); - return (specif.handle != libremidi::port_information{}) || specif.virtualPort; + return (specif.handle.port != libremidi::port_information{}.port) || specif.virtualPort; } }