From bcc3d4265e8b3ed2b17b801923905b60a3927823 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 10 Jan 2026 01:56:34 -0800 Subject: [PATCH] core: switch to custom incubation controller This change requires more QtPrivate usage but eliminates generation or cleanup related window incubation controller bugs. Additionally it enables async loads prior to rendering windows. --- changelog/next.md | 2 + src/core/CMakeLists.txt | 2 +- src/core/generation.cpp | 28 +++------- src/core/generation.hpp | 4 +- src/core/incubator.cpp | 118 ++++++++++++++++++++++++++++++++++++++++ src/core/incubator.hpp | 37 ++++++++++++- src/core/lazyloader.hpp | 3 - 7 files changed, 166 insertions(+), 28 deletions(-) diff --git a/changelog/next.md b/changelog/next.md index e437e6c..3a932ed 100644 --- a/changelog/next.md +++ b/changelog/next.md @@ -36,6 +36,8 @@ set shell id. - Fixed hyprland active toplevel not resetting after window closes. - Fixed hyprland ipc window names and titles being reversed. - Fixed missing signals for system tray item title and description updates. +- Fixed asynchronous loaders not working after reload. +- Fixed asynchronous loaders not working before window creation. ## Packaging Changes diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 472ae04..bbfb8c4 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -51,7 +51,7 @@ qt_add_qml_module(quickshell-core install_qml_module(quickshell-core) -target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::Widgets) +target_link_libraries(quickshell-core PRIVATE Qt::Quick Qt::QuickPrivate Qt::Widgets) qs_module_pch(quickshell-core SET large) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index e15103a..c68af71 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -49,7 +49,8 @@ EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) this->engine->addImportPath("qs:@/"); this->engine->setNetworkAccessManagerFactory(&this->interceptNetFactory); - this->engine->setIncubationController(&this->delayedIncubationController); + this->incubationController.initLoop(); + this->engine->setIncubationController(&this->incubationController); this->engine->addImageProvider("icon", new IconImageProvider()); this->engine->addImageProvider("qsimage", new QsImageProvider()); @@ -134,7 +135,7 @@ void EngineGeneration::onReload(EngineGeneration* old) { // new generation acquires it then incubators will hang intermittently qCDebug(logIncubator) << "Locking incubation controllers of old generation" << old; old->incubationControllersLocked = true; - old->assignIncubationController(); + old->updateIncubationMode(); } QObject::connect(this->engine, &QQmlEngine::quit, this, &EngineGeneration::quit); @@ -288,29 +289,18 @@ void EngineGeneration::trackWindowIncubationController(QQuickWindow* window) { QObject::connect(window, &QObject::destroyed, this, &EngineGeneration::onTrackedWindowDestroyed); this->trackedWindows.append(window); - this->assignIncubationController(); + this->updateIncubationMode(); } void EngineGeneration::onTrackedWindowDestroyed(QObject* object) { this->trackedWindows.removeAll(static_cast(object)); // NOLINT - this->assignIncubationController(); + this->updateIncubationMode(); } -void EngineGeneration::assignIncubationController() { - QQmlIncubationController* controller = &this->delayedIncubationController; - - for (auto* window: this->trackedWindows) { - if (auto* wctl = window->incubationController()) { - controller = wctl; - break; - } - } - - qCDebug(logIncubator) << "Assigning incubation controller" << controller << "to generation" - << this - << "fallback:" << (controller == &this->delayedIncubationController); - - this->engine->setIncubationController(controller); +void EngineGeneration::updateIncubationMode() { + // If we're in a situation with only hidden but tracked windows this might be wrong, + // but it seems to at least work. + this->incubationController.setIncubationMode(!this->trackedWindows.empty()); } EngineGeneration* EngineGeneration::currentGeneration() { diff --git a/src/core/generation.hpp b/src/core/generation.hpp index fef8363..4543408 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -65,7 +65,7 @@ public: QFileSystemWatcher* watcher = nullptr; QVector deletedWatchedFiles; QVector extraWatchedFiles; - DelayedQmlIncubationController delayedIncubationController; + QsIncubationController incubationController; bool reloadComplete = false; QuickshellGlobal* qsgInstance = nullptr; @@ -89,7 +89,7 @@ private slots: private: void postReload(); - void assignIncubationController(); + void updateIncubationMode(); QVector trackedWindows; bool incubationControllersLocked = false; QHash extensions; diff --git a/src/core/incubator.cpp b/src/core/incubator.cpp index c9d149a..f031b11 100644 --- a/src/core/incubator.cpp +++ b/src/core/incubator.cpp @@ -1,7 +1,16 @@ #include "incubator.hpp" +#include +#include +#include #include +#include +#include +#include +#include +#include #include +#include #include #include "logcat.hpp" @@ -15,3 +24,112 @@ void QsQmlIncubator::statusChanged(QQmlIncubator::Status status) { default: break; } } + +void QsIncubationController::initLoop() { + auto* app = static_cast(QGuiApplication::instance()); // NOLINT + this->renderLoop = QSGRenderLoop::instance(); + + QObject::connect( + app, + &QGuiApplication::screenAdded, + this, + &QsIncubationController::updateIncubationTime + ); + + QObject::connect( + app, + &QGuiApplication::screenRemoved, + this, + &QsIncubationController::updateIncubationTime + ); + + this->updateIncubationTime(); + + QObject::connect( + this->renderLoop, + &QSGRenderLoop::timeToIncubate, + this, + &QsIncubationController::incubate + ); + + QAnimationDriver* animationDriver = this->renderLoop->animationDriver(); + if (animationDriver) { + QObject::connect( + animationDriver, + &QAnimationDriver::stopped, + this, + &QsIncubationController::animationStopped + ); + } else { + qCInfo(logIncubator) << "Render loop does not have animation driver, animationStopped cannot " + "be used to trigger incubation."; + } +} + +void QsIncubationController::setIncubationMode(bool render) { + if (render == this->followRenderloop) return; + this->followRenderloop = render; + + if (render) { + qCDebug(logIncubator) << "Incubation mode changed: render loop driven"; + } else { + qCDebug(logIncubator) << "Incubation mode changed: event loop driven"; + } + + if (!render && this->incubatingObjectCount()) this->incubateLater(); +} + +void QsIncubationController::timerEvent(QTimerEvent* /*event*/) { + this->killTimer(this->timerId); + this->timerId = 0; + this->incubate(); +} + +void QsIncubationController::incubateLater() { + if (this->followRenderloop) { + if (this->timerId != 0) { + this->killTimer(this->timerId); + this->timerId = 0; + } + + // Incubate again at the end of the event processing queue + QMetaObject::invokeMethod(this, &QsIncubationController::incubate, Qt::QueuedConnection); + } else if (this->timerId == 0) { + // Wait for a while before processing the next batch. Using a + // timer to avoid starvation of system events. + this->timerId = this->startTimer(this->incubationTime); + } +} + +void QsIncubationController::incubate() { + if ((!this->followRenderloop || this->renderLoop) && this->incubatingObjectCount()) { + if (!this->followRenderloop) { + this->incubateFor(10); + if (this->incubatingObjectCount()) this->incubateLater(); + } else if (this->renderLoop->interleaveIncubation()) { + this->incubateFor(this->incubationTime); + } else { + this->incubateFor(this->incubationTime * 2); + if (this->incubatingObjectCount()) this->incubateLater(); + } + } +} + +void QsIncubationController::animationStopped() { this->incubate(); } + +void QsIncubationController::incubatingObjectCountChanged(int count) { + if (count + && (!this->followRenderloop + || (this->renderLoop && !this->renderLoop->interleaveIncubation()))) + { + this->incubateLater(); + } +} + +void QsIncubationController::updateIncubationTime() { + auto* screen = QGuiApplication::primaryScreen(); + if (!screen) return; + + // 1/3 frame on primary screen + this->incubationTime = qMax(1, static_cast(1000 / screen->refreshRate() / 3)); +} diff --git a/src/core/incubator.hpp b/src/core/incubator.hpp index 5ebb9a0..15dc49a 100644 --- a/src/core/incubator.hpp +++ b/src/core/incubator.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -25,7 +26,37 @@ signals: void failed(); }; -class DelayedQmlIncubationController: public QQmlIncubationController { - // Do nothing. - // This ensures lazy loaders don't start blocking before onReload creates windows. +class QSGRenderLoop; + +class QsIncubationController + : public QObject + , public QQmlIncubationController { + Q_OBJECT + +public: + void initLoop(); + void setIncubationMode(bool render); + void incubateLater(); + +protected: + void timerEvent(QTimerEvent* event) override; + +public slots: + void incubate(); + void animationStopped(); + void updateIncubationTime(); + +protected: + void incubatingObjectCountChanged(int count) override; + +private: +// QPointer did not work with forward declarations prior to 6.7 +#if QT_VERSION >= QT_VERSION_CHECK(6, 7, 0) + QPointer renderLoop = nullptr; +#else + QSGRenderLoop* renderLoop = nullptr; +#endif + int incubationTime = 0; + int timerId = 0; + bool followRenderloop = false; }; diff --git a/src/core/lazyloader.hpp b/src/core/lazyloader.hpp index dbaad4b..56cc964 100644 --- a/src/core/lazyloader.hpp +++ b/src/core/lazyloader.hpp @@ -82,9 +82,6 @@ /// > Notably, @@Variants does not corrently support asynchronous /// > loading, meaning using it inside a LazyLoader will block similarly to not /// > having a loader to start with. -/// -/// > [!WARNING] LazyLoaders do not start loading before the first window is created, -/// > meaning if you create all windows inside of lazy loaders, none of them will ever load. class LazyLoader: public Reloadable { Q_OBJECT; /// The fully loaded item if the loader is @@loading or @@active, or `null`