Files
o3de/Code/Editor/Core/QtEditorApplication.cpp
T
Chris Burel d2cdb511d0 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 <burelc@amazon.com>
2022-02-14 14:22:07 -08:00

476 lines
16 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include "EditorDefs.h"
#include "QtEditorApplication.h"
// Qt
#include <QAbstractEventDispatcher>
#include <QScopedValueRollback>
#include <QToolBar>
#include <QLoggingCategory>
#include <AzCore/Component/ComponentApplication.h>
#include <AzCore/IO/Path/Path.h>
#include <AzCore/Settings/SettingsRegistryMergeUtils.h>
// AzQtComponents
#include <AzQtComponents/Components/GlobalEventFilter.h>
#include <AzQtComponents/Components/O3DEStylesheet.h>
#include <AzQtComponents/Components/Titlebar.h>
#include <AzQtComponents/Components/WindowDecorationWrapper.h>
// Editor
#include "Settings.h"
#include "CryEdit.h"
Q_LOGGING_CATEGORY(InputDebugging, "o3de.editor.input")
// internal, private namespace:
namespace
{
class EditorGlobalEventFilter
: public AzQtComponents::GlobalEventFilter
{
public:
explicit EditorGlobalEventFilter(QObject* watch)
: AzQtComponents::GlobalEventFilter(watch) {}
bool eventFilter(QObject* obj, QEvent* e) override
{
static bool isRecursing = false;
if (isRecursing)
{
return false;
}
QScopedValueRollback<bool> guard(isRecursing, true);
// Detect Widget move
// We're doing this before the events are actually consumed to avoid confusion
if (IsDragGuardedWidget(obj))
{
switch (e->type())
{
case QEvent::MouseButtonPress:
{
m_widgetDraggedState = WidgetDraggedState::Clicked;
break;
}
case QEvent::Move:
case QEvent::MouseMove:
{
if (m_widgetDraggedState == WidgetDraggedState::Clicked)
{
m_widgetDraggedState = WidgetDraggedState::Dragged;
}
break;
}
}
}
if (e->type() == QEvent::MouseButtonRelease)
{
m_widgetDraggedState = WidgetDraggedState::None;
}
switch (e->type())
{
case QEvent::KeyPress:
case QEvent::KeyRelease:
{
if (GetIEditor()->IsInGameMode())
{
// don't let certain keys fall through to the game when it's running
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(e);
auto key = keyEvent->key();
if ((key == Qt::Key_Alt) || (key == Qt::Key_AltGr) || ((key >= Qt::Key_F1) && (key <= Qt::Key_F35)))
{
return true;
}
}
}
break;
case QEvent::Shortcut:
{
// Eat shortcuts in game mode or when a guarded widget is being dragged
if (GetIEditor()->IsInGameMode() || m_widgetDraggedState == WidgetDraggedState::Dragged)
{
return true;
}
}
break;
case QEvent::MouseButtonPress:
case QEvent::MouseButtonRelease:
case QEvent::MouseButtonDblClick:
case QEvent::MouseMove:
{
#if AZ_TRAIT_OS_PLATFORM_APPLE
auto widget = qobject_cast<QWidget*>(obj);
if (widget && widget->graphicsProxyWidget() != nullptr)
{
QMouseEvent* me = static_cast<QMouseEvent*>(e);
QWidget* target = qApp->widgetAt(QCursor::pos());
if (target)
{
QMouseEvent ev(me->type(), target->mapFromGlobal(QCursor::pos()), me->button(), me->buttons(), me->modifiers());
qApp->notify(target, &ev);
return true;
}
}
#endif
GuardMouseEventSelectionChangeMetrics(e);
}
break;
}
return GlobalEventFilter::eventFilter(obj, e);
}
private:
bool m_mouseButtonWasDown = false;
void GuardMouseEventSelectionChangeMetrics(QEvent* e)
{
// Force the metrics collector to queue up any selection changed metrics until mouse release, so that we don't
// get flooded with multiple selection changed events when one, sent on mouse release, is enough.
if (e->type() == QEvent::MouseButtonPress)
{
if (!m_mouseButtonWasDown)
{
m_mouseButtonWasDown = true;
}
}
else if (e->type() == QEvent::MouseButtonRelease)
{
// This is a tricky case. We don't want to send the end selection change event too early
// because there might be other things responding to the mouse release after this, and we want to
// block handling of the selection change events until we're entirely finished with the mouse press.
// So, queue the handling with a single shot timer, but then check the state of the mouse buttons
// to ensure that they haven't been pressed in between the release and the timer firing off.
QTimer::singleShot(0, this, [this]() {
if (!QApplication::mouseButtons() && m_mouseButtonWasDown)
{
m_mouseButtonWasDown = false;
}
});
}
}
//! Detect if the event's target is a Widget we want to guard from shortcuts while it's being dragged.
//! This function can be easily expanded to handle exceptions.
bool IsDragGuardedWidget(const QObject* obj)
{
return qobject_cast<const QWidget*>(obj) != nullptr;
}
//! Enum to keep track of Widget dragged state
enum class WidgetDraggedState
{
None, //!< No widget is being clicked nor dragged
Clicked, //!< A widget has been clicked on but has not been dragged
Dragged, //!< A widget is being dragged
};
WidgetDraggedState m_widgetDraggedState = WidgetDraggedState::None;
};
static void LogToDebug([[maybe_unused]] QtMsgType Type, [[maybe_unused]] const QMessageLogContext& Context, const QString& message)
{
AZ::Debug::Platform::OutputToDebugger("Qt", message.toUtf8().data());
AZ::Debug::Platform::OutputToDebugger(nullptr, "\n");
}
}
namespace Editor
{
void ScanDirectories(QFileInfoList& directoryList, const QStringList& filters, QFileInfoList& files, ScanDirectoriesUpdateCallBack updateCallback)
{
while (!directoryList.isEmpty())
{
QDir directory(directoryList.front().absoluteFilePath(), "*", QDir::Name | QDir::IgnoreCase, QDir::AllEntries);
directoryList.pop_front();
if (directory.exists())
{
// Append each file from this directory that matches one of the filters to files
directory.setNameFilters(filters);
directory.setFilter(QDir::Files);
files.append(directory.entryInfoList());
// Add all of the subdirectories from this directory to the queue to be searched
directory.setNameFilters(QStringList("*"));
directory.setFilter(QDir::Dirs | QDir::NoDotAndDotDot);
directoryList.append(directory.entryInfoList());
if (updateCallback)
{
updateCallback();
}
}
}
}
EditorQtApplication::EditorQtApplication(int& argc, char** argv)
: AzQtApplication(argc, argv)
, m_stylesheet(new AzQtComponents::O3DEStylesheet(this))
{
setWindowIcon(QIcon(":/Application/res/o3de_editor.ico"));
// set the default key store for our preferences:
setApplicationName("O3DE Editor");
installEventFilter(this);
// Disable our debugging input helpers by default
QLoggingCategory::setFilterRules(QStringLiteral("o3de.editor.input.*=false"));
// Initialize our stylesheet here to allow Gems to register stylesheets when their system components activate.
AZ::IO::FixedMaxPath engineRootPath;
{
// Create a ComponentApplication to initialize the AZ::SystemAllocator and initialize the SettingsRegistry
AZ::ComponentApplication application(argc, argv);
auto settingsRegistry = AZ::SettingsRegistry::Get();
settingsRegistry->Get(engineRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_EngineRootFolder);
}
m_stylesheet->initialize(this, engineRootPath);
}
void EditorQtApplication::Initialize()
{
GetIEditor()->RegisterNotifyListener(this);
// install QTranslator
InstallEditorTranslators();
// install hooks and filters last and revoke them first
InstallFilters();
// install this filter. It will be a parent of the application and cleaned up when it is cleaned up automically
auto globalEventFilter = new EditorGlobalEventFilter(this);
installEventFilter(globalEventFilter);
}
void EditorQtApplication::LoadSettings()
{
AZ::SerializeContext* context;
EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(context, "No serialize context");
char resolvedPath[AZ_MAX_PATH_LEN];
AZ::IO::FileIOBase::GetInstance()->ResolvePath("@user@/EditorUserSettings.xml", resolvedPath, AZ_MAX_PATH_LEN);
m_localUserSettings.Load(resolvedPath, context);
m_localUserSettings.Activate(AZ::UserSettings::CT_LOCAL);
AZ::UserSettingsOwnerRequestBus::Handler::BusConnect(AZ::UserSettings::CT_LOCAL);
m_activatedLocalUserSettings = true;
}
void EditorQtApplication::UnloadSettings()
{
if (m_activatedLocalUserSettings)
{
SaveSettings();
m_localUserSettings.Deactivate();
AZ::UserSettingsOwnerRequestBus::Handler::BusDisconnect();
m_activatedLocalUserSettings = false;
}
}
void EditorQtApplication::SaveSettings()
{
if (m_activatedLocalUserSettings)
{
AZ::SerializeContext* context;
EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(context, "No serialize context");
char resolvedPath[AZ_MAX_PATH_LEN];
AZ::IO::FileIOBase::GetInstance()->ResolvePath("@user@/EditorUserSettings.xml", resolvedPath, AZ_ARRAY_SIZE(resolvedPath));
m_localUserSettings.Save(resolvedPath, context);
}
}
void EditorQtApplication::maybeProcessIdle()
{
if (!m_isMovingOrResizing)
{
if (auto winapp = CCryEditApp::instance())
{
winapp->OnIdle(0);
}
}
if (m_applicationActive)
{
QTimer::singleShot(1, this, &EditorQtApplication::maybeProcessIdle);
}
}
void EditorQtApplication::InstallQtLogHandler()
{
qInstallMessageHandler(LogToDebug);
}
void EditorQtApplication::InstallFilters()
{
if (auto dispatcher = QAbstractEventDispatcher::instance())
{
dispatcher->installNativeEventFilter(this);
}
}
void EditorQtApplication::UninstallFilters()
{
if (auto dispatcher = QAbstractEventDispatcher::instance())
{
dispatcher->removeNativeEventFilter(this);
}
}
EditorQtApplication::~EditorQtApplication()
{
if (GetIEditor())
{
GetIEditor()->UnregisterNotifyListener(this);
}
UninstallFilters();
UninstallEditorTranslators();
}
EditorQtApplication* EditorQtApplication::instance()
{
return static_cast<EditorQtApplication*>(QApplication::instance());
}
void EditorQtApplication::OnEditorNotifyEvent(EEditorNotifyEvent event)
{
switch (event)
{
case eNotify_OnStyleChanged:
RefreshStyleSheet();
emit skinChanged();
break;
case eNotify_OnQuit:
GetIEditor()->UnregisterNotifyListener(this);
break;
}
}
QColor EditorQtApplication::InterpolateColors(QColor a, QColor b, float factor)
{
return QColor(int(a.red() * (1.0f - factor) + b.red() * factor),
int(a.green() * (1.0f - factor) + b.green() * factor),
int(a.blue() * (1.0f - factor) + b.blue() * factor),
int(a.alpha() * (1.0f - factor) + b.alpha() * factor));
}
void EditorQtApplication::RefreshStyleSheet()
{
m_stylesheet->Refresh();
}
void EditorQtApplication::setIsMovingOrResizing(bool isMovingOrResizing)
{
if (m_isMovingOrResizing == isMovingOrResizing)
{
return;
}
m_isMovingOrResizing = isMovingOrResizing;
}
bool EditorQtApplication::isMovingOrResizing() const
{
return m_isMovingOrResizing;
}
const QColor& EditorQtApplication::GetColorByName(const QString& name)
{
return m_stylesheet->GetColorByName(name);
}
bool EditorQtApplication::IsActive()
{
return applicationState() == Qt::ApplicationActive;
}
QTranslator* EditorQtApplication::CreateAndInitializeTranslator(const QString& filename, const QString& directory)
{
Q_ASSERT(QFile::exists(directory + "/" + filename));
QTranslator* translator = new QTranslator();
translator->load(filename, directory);
installTranslator(translator);
return translator;
}
void EditorQtApplication::InstallEditorTranslators()
{
m_editorTranslator = CreateAndInitializeTranslator("editor_en-us.qm", ":/Translations");
m_assetBrowserTranslator = CreateAndInitializeTranslator("assetbrowser_en-us.qm", ":/Translations");
}
void EditorQtApplication::DeleteTranslator(QTranslator*& translator)
{
removeTranslator(translator);
delete translator;
translator = nullptr;
}
void EditorQtApplication::UninstallEditorTranslators()
{
DeleteTranslator(m_editorTranslator);
DeleteTranslator(m_assetBrowserTranslator);
}
void EditorQtApplication::EnableOnIdle(bool enable)
{
m_applicationActive = enable;
if (enable)
{
QTimer::singleShot(0, this, &EditorQtApplication::maybeProcessIdle);
}
}
bool EditorQtApplication::OnIdleEnabled() const
{
return m_applicationActive;
}
bool EditorQtApplication::eventFilter(QObject* object, QEvent* event)
{
switch (event->type())
{
case QEvent::MouseButtonPress:
m_pressedButtons |= reinterpret_cast<QMouseEvent*>(event)->button();
break;
case QEvent::MouseButtonRelease:
m_pressedButtons &= ~(reinterpret_cast<QMouseEvent*>(event)->button());
break;
case QEvent::KeyPress:
m_pressedKeys.insert(reinterpret_cast<QKeyEvent*>(event)->key());
break;
case QEvent::KeyRelease:
m_pressedKeys.remove(reinterpret_cast<QKeyEvent*>(event)->key());
break;
default:
break;
}
return QApplication::eventFilter(object, event);
}
} // end namespace Editor
#include <Core/moc_QtEditorApplication.cpp>