From d2cdb511d0d4e439c5a89f747086ff5baca2b046 Mon Sep 17 00:00:00 2001 From: Chris Burel Date: Mon, 14 Feb 2022 14:22:07 -0800 Subject: [PATCH] Improve the framerate of Editor widgets that update in the TickBus (#7408) Previously, with no level loaded in the main editor, and no assets loaded in the EMotionFX editor, the AnimGraph viewport would only update at 15fps, even on a 64 core machine. Digging into this with Pix, I found that EMotionFX's UI would only get updated for every other call to the ComponentApplication tick. The Editor's QApplication instance controls how the `ComponentApplication::OnTick` method is called. This is done in the `maybeProcessIdle()` method. Generally, I found callstacks like this: ``` EditorQtApplication::maybeProcessIdle() CCryEditApp::OnIdle() CCryEditApp::IdleProcessing() CGameEngine::Update() # ~2.2ms PhysX::Update() # ~1.8ms EMotionFX::Update() # ~0.2ms calls QWidget::update() on all the widgets that change per frame, only puts update events on the event queue, doesn't paint anything AZ::ComponentApplication::TickSystem() # ~25ms renders the frame with Atom # ~24ms ``` The `maybeProcessIdle()` method is invoked by a QTimer, with a timeout that changes depending on the application state. If the Editor is in game mode, it used a timeout of 0, which essentially forced the game to run at as high an fps as possible. If the Editor application has focus, it used a timeout of 1ms, asking for the idle processing to happen at 1000 fps. Otherwise, it used a timeout of 10ms, asking for idle processing at 100 fps. Those fps targets are not realistic. What happened in this case is that while the PhysX system was being updated, while the previous `maybeProcessIdle()` call was still processing, the idle processing timer would timeout again, and place a timer event on Qt's event queue, before anything else had a chance to do anything. Only afterward would EMotionFX's MainWindow::OnTick would be invoked, placing paint events on Qt's event queue. The fix for this is to use a single shot timer at the end of each `maybeProcessIdle()` call. This ensures that any events that are enqueued during the `maybeProcessIdle()` call are processed prior to the next call to `maybeProcessIdle()`. Signed-off-by: Chris Burel --- Code/Editor/Core/QtEditorApplication.cpp | 72 +++--------------------- Code/Editor/Core/QtEditorApplication.h | 9 +-- 2 files changed, 8 insertions(+), 73 deletions(-) diff --git a/Code/Editor/Core/QtEditorApplication.cpp b/Code/Editor/Core/QtEditorApplication.cpp index 2439228476..8f7db74125 100644 --- a/Code/Editor/Core/QtEditorApplication.cpp +++ b/Code/Editor/Core/QtEditorApplication.cpp @@ -32,15 +32,6 @@ #include "Settings.h" #include "CryEdit.h" -enum -{ - // in milliseconds - GameModeIdleFrequency = 0, - EditorModeIdleFrequency = 1, - InactiveModeFrequency = 10, - UninitializedFrequency = 9999, -}; - Q_LOGGING_CATEGORY(InputDebugging, "o3de.editor.input") // internal, private namespace: @@ -234,18 +225,12 @@ namespace Editor EditorQtApplication::EditorQtApplication(int& argc, char** argv) : AzQtApplication(argc, argv) , m_stylesheet(new AzQtComponents::O3DEStylesheet(this)) - , m_idleTimer(new QTimer(this)) { - m_idleTimer->setInterval(UninitializedFrequency); - setWindowIcon(QIcon(":/Application/res/o3de_editor.ico")); // set the default key store for our preferences: setApplicationName("O3DE Editor"); - connect(m_idleTimer, &QTimer::timeout, this, &EditorQtApplication::maybeProcessIdle); - - connect(this, &QGuiApplication::applicationStateChanged, this, [this] { ResetIdleTimerInterval(PollState); }); installEventFilter(this); // Disable our debugging input helpers by default @@ -324,6 +309,10 @@ namespace Editor winapp->OnIdle(0); } } + if (m_applicationActive) + { + QTimer::singleShot(1, this, &EditorQtApplication::maybeProcessIdle); + } } void EditorQtApplication::InstallQtLogHandler() @@ -376,14 +365,6 @@ namespace Editor case eNotify_OnQuit: GetIEditor()->UnregisterNotifyListener(this); break; - - case eNotify_OnBeginGameMode: - // GetIEditor()->IsInGameMode() Isn't reliable when called from within the notification handler - ResetIdleTimerInterval(GameMode); - break; - case eNotify_OnEndGameMode: - ResetIdleTimerInterval(EditorMode); - break; } } @@ -456,55 +437,16 @@ namespace Editor void EditorQtApplication::EnableOnIdle(bool enable) { + m_applicationActive = enable; if (enable) { - if (m_idleTimer->interval() == UninitializedFrequency) - { - ResetIdleTimerInterval(); - } - - m_idleTimer->start(); - } - else - { - m_idleTimer->stop(); + QTimer::singleShot(0, this, &EditorQtApplication::maybeProcessIdle); } } bool EditorQtApplication::OnIdleEnabled() const { - if (m_idleTimer->interval() == UninitializedFrequency) - { - return false; - } - - return m_idleTimer->isActive(); - } - - void EditorQtApplication::ResetIdleTimerInterval(TimerResetFlag flag) - { - bool isInGameMode = flag == GameMode; - if (flag == PollState) - { - isInGameMode = GetIEditor() ? GetIEditor()->IsInGameMode() : false; - } - - // Game mode takes precedence over anything else - if (isInGameMode) - { - m_idleTimer->setInterval(GameModeIdleFrequency); - } - else - { - if (applicationState() & Qt::ApplicationActive) - { - m_idleTimer->setInterval(EditorModeIdleFrequency); - } - else - { - m_idleTimer->setInterval(InactiveModeFrequency); - } - } + return m_applicationActive; } bool EditorQtApplication::eventFilter(QObject* object, QEvent* event) diff --git a/Code/Editor/Core/QtEditorApplication.h b/Code/Editor/Core/QtEditorApplication.h index 28ee8ac14b..9cb03ec644 100644 --- a/Code/Editor/Core/QtEditorApplication.h +++ b/Code/Editor/Core/QtEditorApplication.h @@ -102,13 +102,6 @@ namespace Editor bool m_isMovingOrResizing = false; private: - enum TimerResetFlag - { - PollState, - GameMode, - EditorMode - }; - void ResetIdleTimerInterval(TimerResetFlag = PollState); static QColor InterpolateColors(QColor a, QColor b, float factor); void RefreshStyleSheet(); void InstallFilters(); @@ -125,7 +118,6 @@ namespace Editor QTranslator* m_editorTranslator = nullptr; QTranslator* m_assetBrowserTranslator = nullptr; - QTimer* const m_idleTimer = nullptr; AZ::UserSettingsProvider m_localUserSettings; @@ -133,5 +125,6 @@ namespace Editor QSet m_pressedKeys; bool m_activatedLocalUserSettings = false; + bool m_applicationActive = false; }; } // namespace editor