Merge branch 'development' of https://github.com/o3de/o3de into jckand/DynVegXfailFixes
This commit is contained in:
@@ -49,7 +49,6 @@ class TestViewMenuOptions(EditorTestHelper):
|
||||
("Viewport", "Center on Selection"),
|
||||
("Viewport", "Go to Location"),
|
||||
("Viewport", "Remember Location"),
|
||||
("Viewport", "Change Move Speed"),
|
||||
("Viewport", "Switch Camera"),
|
||||
("Viewport", "Show/Hide Helpers"),
|
||||
("Refresh Style",),
|
||||
|
||||
@@ -89,7 +89,6 @@ class TestMenus(object):
|
||||
"Center on Selection Action triggered",
|
||||
"Go to Location Action triggered",
|
||||
"Remember Location Action triggered",
|
||||
"Change Move Speed Action triggered",
|
||||
"Switch Camera Action triggered",
|
||||
"Show/Hide Helpers Action triggered",
|
||||
"Refresh Style Action triggered",
|
||||
|
||||
@@ -714,7 +714,7 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
|
||||
|
||||
if (m_renderViewport)
|
||||
{
|
||||
m_renderViewport->GetControllerList()->SetEnabled(false);
|
||||
m_renderViewport->SetInputProcessingEnabled(false);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -738,7 +738,7 @@ void EditorViewportWidget::OnEditorNotifyEvent(EEditorNotifyEvent event)
|
||||
|
||||
if (m_renderViewport)
|
||||
{
|
||||
m_renderViewport->GetControllerList()->SetEnabled(true);
|
||||
m_renderViewport->SetInputProcessingEnabled(true);
|
||||
}
|
||||
break;
|
||||
|
||||
|
||||
@@ -404,7 +404,7 @@ bool LegacyViewportCameraControllerInstance::HandleInputChannelEvent(const AzFra
|
||||
}
|
||||
else if (auto key = GetKeyboardKey(event.m_inputChannel); key != Qt::Key_unknown)
|
||||
{
|
||||
if (state == InputChannel::State::Ended)
|
||||
if (!event.m_inputChannel.IsActive())
|
||||
{
|
||||
m_pressedKeys.erase(key);
|
||||
}
|
||||
|
||||
@@ -216,7 +216,8 @@ namespace SandboxEditor
|
||||
interactionHandled, AzToolsFramework::GetEntityContextId(), targetInteractionEvent, mouseInteractionEvent);
|
||||
}
|
||||
|
||||
return interactionHandled;
|
||||
// Only filter button/key press events, not release events
|
||||
return interactionHandled && event.m_inputChannel.IsActive();
|
||||
}
|
||||
|
||||
void ViewportManipulatorControllerInstance::ResetInputChannels()
|
||||
|
||||
+2
-2
@@ -454,8 +454,8 @@ namespace AzFramework
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
InputDeviceKeyboard::InputDeviceKeyboard()
|
||||
: InputDevice(Id)
|
||||
InputDeviceKeyboard::InputDeviceKeyboard(AzFramework::InputDeviceId id)
|
||||
: InputDevice(id)
|
||||
, m_modifierKeyStates(AZStd::make_shared<ModifierKeyStates>())
|
||||
, m_allChannelsById()
|
||||
, m_keyChannelsById()
|
||||
|
||||
@@ -240,7 +240,7 @@ namespace AzFramework
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! Constructor
|
||||
InputDeviceKeyboard();
|
||||
InputDeviceKeyboard(AzFramework::InputDeviceId id = Id);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Disable copying
|
||||
|
||||
@@ -95,8 +95,8 @@ namespace AzFramework
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
InputDeviceMouse::InputDeviceMouse()
|
||||
: InputDevice(Id)
|
||||
InputDeviceMouse::InputDeviceMouse(AzFramework::InputDeviceId id)
|
||||
: InputDevice(id)
|
||||
, m_allChannelsById()
|
||||
, m_buttonChannelsById()
|
||||
, m_movementChannelsById()
|
||||
|
||||
@@ -111,7 +111,7 @@ namespace AzFramework
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
//! Constructor
|
||||
explicit InputDeviceMouse();
|
||||
explicit InputDeviceMouse(AzFramework::InputDeviceId id = Id);
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Disable copying
|
||||
|
||||
@@ -747,19 +747,24 @@ namespace AzFramework
|
||||
return button == inputChannelId;
|
||||
});
|
||||
|
||||
if (inputChannelId == InputDeviceMouse::Movement::X)
|
||||
// accept active mouse channel updates, inactive movement channels will just have a 0 delta
|
||||
if (inputChannel.IsActive())
|
||||
{
|
||||
return HorizontalMotionEvent{ aznumeric_cast<int>(inputChannel.GetValue()) };
|
||||
if (inputChannelId == InputDeviceMouse::Movement::X)
|
||||
{
|
||||
return HorizontalMotionEvent{ aznumeric_cast<int>(inputChannel.GetValue()) };
|
||||
}
|
||||
else if (inputChannelId == InputDeviceMouse::Movement::Y)
|
||||
{
|
||||
return VerticalMotionEvent{ aznumeric_cast<int>(inputChannel.GetValue()) };
|
||||
}
|
||||
else if (inputChannelId == InputDeviceMouse::Movement::Z)
|
||||
{
|
||||
return ScrollEvent{ inputChannel.GetValue() };
|
||||
}
|
||||
}
|
||||
else if (inputChannelId == InputDeviceMouse::Movement::Y)
|
||||
{
|
||||
return VerticalMotionEvent{ aznumeric_cast<int>(inputChannel.GetValue()) };
|
||||
}
|
||||
else if (inputChannelId == InputDeviceMouse::Movement::Z)
|
||||
{
|
||||
return ScrollEvent{ inputChannel.GetValue() };
|
||||
}
|
||||
else if (wasMouseButton || InputDeviceKeyboard::IsKeyboardDevice(inputDeviceId))
|
||||
|
||||
if (wasMouseButton || InputDeviceKeyboard::IsKeyboardDevice(inputDeviceId))
|
||||
{
|
||||
return DiscreteInputEvent{ inputChannelId, inputChannel.GetState() };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,397 @@
|
||||
/*
|
||||
* 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 <AzToolsFramework/Input/QtEventToAzInputManager.h>
|
||||
|
||||
#include <AzCore/std/smart_ptr/make_shared.h>
|
||||
|
||||
#include <AzFramework/Input/Buses/Notifications/InputChannelNotificationBus.h>
|
||||
#include <AzFramework/Input/Buses/Requests/InputChannelRequestBus.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QCursor>
|
||||
#include <QEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QWheelEvent>
|
||||
#include <QWidget>
|
||||
|
||||
namespace AzToolsFramework
|
||||
{
|
||||
void QtEventToAzInputMapper::InitializeKeyMappings()
|
||||
{
|
||||
// This assumes modifier keys (ctrl/shift/alt) map to the left control/shift/alt keys as Qt provides no way to disambiguate
|
||||
// in a platform agnostic manner. This could be expanded later with a PAL mapping from native scan codes acquired from
|
||||
// QKeyEvents, if needed.
|
||||
m_keyMappings = { {
|
||||
{ Qt::Key_0, AzFramework::InputDeviceKeyboard::Key::Alphanumeric0 },
|
||||
{ Qt::Key_1, AzFramework::InputDeviceKeyboard::Key::Alphanumeric1 },
|
||||
{ Qt::Key_2, AzFramework::InputDeviceKeyboard::Key::Alphanumeric2 },
|
||||
{ Qt::Key_3, AzFramework::InputDeviceKeyboard::Key::Alphanumeric3 },
|
||||
{ Qt::Key_4, AzFramework::InputDeviceKeyboard::Key::Alphanumeric4 },
|
||||
{ Qt::Key_5, AzFramework::InputDeviceKeyboard::Key::Alphanumeric5 },
|
||||
{ Qt::Key_6, AzFramework::InputDeviceKeyboard::Key::Alphanumeric6 },
|
||||
{ Qt::Key_7, AzFramework::InputDeviceKeyboard::Key::Alphanumeric7 },
|
||||
{ Qt::Key_8, AzFramework::InputDeviceKeyboard::Key::Alphanumeric8 },
|
||||
{ Qt::Key_9, AzFramework::InputDeviceKeyboard::Key::Alphanumeric9 },
|
||||
{ Qt::Key_A, AzFramework::InputDeviceKeyboard::Key::AlphanumericA },
|
||||
{ Qt::Key_B, AzFramework::InputDeviceKeyboard::Key::AlphanumericB },
|
||||
{ Qt::Key_C, AzFramework::InputDeviceKeyboard::Key::AlphanumericC },
|
||||
{ Qt::Key_D, AzFramework::InputDeviceKeyboard::Key::AlphanumericD },
|
||||
{ Qt::Key_E, AzFramework::InputDeviceKeyboard::Key::AlphanumericE },
|
||||
{ Qt::Key_F, AzFramework::InputDeviceKeyboard::Key::AlphanumericF },
|
||||
{ Qt::Key_G, AzFramework::InputDeviceKeyboard::Key::AlphanumericG },
|
||||
{ Qt::Key_H, AzFramework::InputDeviceKeyboard::Key::AlphanumericH },
|
||||
{ Qt::Key_I, AzFramework::InputDeviceKeyboard::Key::AlphanumericI },
|
||||
{ Qt::Key_J, AzFramework::InputDeviceKeyboard::Key::AlphanumericJ },
|
||||
{ Qt::Key_K, AzFramework::InputDeviceKeyboard::Key::AlphanumericK },
|
||||
{ Qt::Key_L, AzFramework::InputDeviceKeyboard::Key::AlphanumericL },
|
||||
{ Qt::Key_M, AzFramework::InputDeviceKeyboard::Key::AlphanumericM },
|
||||
{ Qt::Key_N, AzFramework::InputDeviceKeyboard::Key::AlphanumericN },
|
||||
{ Qt::Key_O, AzFramework::InputDeviceKeyboard::Key::AlphanumericO },
|
||||
{ Qt::Key_P, AzFramework::InputDeviceKeyboard::Key::AlphanumericP },
|
||||
{ Qt::Key_Q, AzFramework::InputDeviceKeyboard::Key::AlphanumericQ },
|
||||
{ Qt::Key_R, AzFramework::InputDeviceKeyboard::Key::AlphanumericR },
|
||||
{ Qt::Key_S, AzFramework::InputDeviceKeyboard::Key::AlphanumericS },
|
||||
{ Qt::Key_T, AzFramework::InputDeviceKeyboard::Key::AlphanumericT },
|
||||
{ Qt::Key_U, AzFramework::InputDeviceKeyboard::Key::AlphanumericU },
|
||||
{ Qt::Key_V, AzFramework::InputDeviceKeyboard::Key::AlphanumericV },
|
||||
{ Qt::Key_W, AzFramework::InputDeviceKeyboard::Key::AlphanumericW },
|
||||
{ Qt::Key_X, AzFramework::InputDeviceKeyboard::Key::AlphanumericX },
|
||||
{ Qt::Key_Y, AzFramework::InputDeviceKeyboard::Key::AlphanumericY },
|
||||
{ Qt::Key_Z, AzFramework::InputDeviceKeyboard::Key::AlphanumericZ },
|
||||
{ Qt::Key_Backspace, AzFramework::InputDeviceKeyboard::Key::EditBackspace },
|
||||
{ Qt::Key_CapsLock, AzFramework::InputDeviceKeyboard::Key::EditCapsLock },
|
||||
{ Qt::Key_Enter, AzFramework::InputDeviceKeyboard::Key::EditEnter },
|
||||
{ Qt::Key_Space, AzFramework::InputDeviceKeyboard::Key::EditSpace },
|
||||
{ Qt::Key_Tab, AzFramework::InputDeviceKeyboard::Key::EditTab },
|
||||
{ Qt::Key_Escape, AzFramework::InputDeviceKeyboard::Key::Escape },
|
||||
{ Qt::Key_F1, AzFramework::InputDeviceKeyboard::Key::Function01 },
|
||||
{ Qt::Key_F2, AzFramework::InputDeviceKeyboard::Key::Function02 },
|
||||
{ Qt::Key_F3, AzFramework::InputDeviceKeyboard::Key::Function03 },
|
||||
{ Qt::Key_F4, AzFramework::InputDeviceKeyboard::Key::Function04 },
|
||||
{ Qt::Key_F5, AzFramework::InputDeviceKeyboard::Key::Function05 },
|
||||
{ Qt::Key_F6, AzFramework::InputDeviceKeyboard::Key::Function06 },
|
||||
{ Qt::Key_F7, AzFramework::InputDeviceKeyboard::Key::Function07 },
|
||||
{ Qt::Key_F8, AzFramework::InputDeviceKeyboard::Key::Function08 },
|
||||
{ Qt::Key_F9, AzFramework::InputDeviceKeyboard::Key::Function09 },
|
||||
{ Qt::Key_F10, AzFramework::InputDeviceKeyboard::Key::Function10 },
|
||||
{ Qt::Key_F11, AzFramework::InputDeviceKeyboard::Key::Function11 },
|
||||
{ Qt::Key_F12, AzFramework::InputDeviceKeyboard::Key::Function12 },
|
||||
{ Qt::Key_F13, AzFramework::InputDeviceKeyboard::Key::Function13 },
|
||||
{ Qt::Key_F14, AzFramework::InputDeviceKeyboard::Key::Function14 },
|
||||
{ Qt::Key_F15, AzFramework::InputDeviceKeyboard::Key::Function15 },
|
||||
{ Qt::Key_F16, AzFramework::InputDeviceKeyboard::Key::Function16 },
|
||||
{ Qt::Key_F17, AzFramework::InputDeviceKeyboard::Key::Function17 },
|
||||
{ Qt::Key_F18, AzFramework::InputDeviceKeyboard::Key::Function18 },
|
||||
{ Qt::Key_F19, AzFramework::InputDeviceKeyboard::Key::Function19 },
|
||||
{ Qt::Key_F20, AzFramework::InputDeviceKeyboard::Key::Function20 },
|
||||
{ Qt::Key_Alt, AzFramework::InputDeviceKeyboard::Key::ModifierAltL },
|
||||
{ Qt::Key_Control, AzFramework::InputDeviceKeyboard::Key::ModifierCtrlL },
|
||||
{ Qt::Key_Shift, AzFramework::InputDeviceKeyboard::Key::ModifierShiftL },
|
||||
{ Qt::Key_Super_L, AzFramework::InputDeviceKeyboard::Key::ModifierSuperL },
|
||||
{ Qt::Key_Super_R, AzFramework::InputDeviceKeyboard::Key::ModifierSuperR },
|
||||
{ Qt::Key_Down, AzFramework::InputDeviceKeyboard::Key::NavigationArrowDown },
|
||||
{ Qt::Key_Left, AzFramework::InputDeviceKeyboard::Key::NavigationArrowLeft },
|
||||
{ Qt::Key_Right, AzFramework::InputDeviceKeyboard::Key::NavigationArrowRight },
|
||||
{ Qt::Key_Up, AzFramework::InputDeviceKeyboard::Key::NavigationArrowUp },
|
||||
{ Qt::Key_Delete, AzFramework::InputDeviceKeyboard::Key::NavigationDelete },
|
||||
{ Qt::Key_End, AzFramework::InputDeviceKeyboard::Key::NavigationEnd },
|
||||
{ Qt::Key_Home, AzFramework::InputDeviceKeyboard::Key::NavigationHome },
|
||||
{ Qt::Key_Insert, AzFramework::InputDeviceKeyboard::Key::NavigationInsert },
|
||||
{ Qt::Key_PageDown, AzFramework::InputDeviceKeyboard::Key::NavigationPageDown },
|
||||
{ Qt::Key_PageUp, AzFramework::InputDeviceKeyboard::Key::NavigationPageUp },
|
||||
{ Qt::Key_Apostrophe, AzFramework::InputDeviceKeyboard::Key::PunctuationApostrophe },
|
||||
{ Qt::Key_Backslash, AzFramework::InputDeviceKeyboard::Key::PunctuationBackslash },
|
||||
{ Qt::Key_BracketLeft, AzFramework::InputDeviceKeyboard::Key::PunctuationBracketL },
|
||||
{ Qt::Key_BracketRight, AzFramework::InputDeviceKeyboard::Key::PunctuationBracketR },
|
||||
{ Qt::Key_Comma, AzFramework::InputDeviceKeyboard::Key::PunctuationComma },
|
||||
{ Qt::Key_Equal, AzFramework::InputDeviceKeyboard::Key::PunctuationEquals },
|
||||
{ Qt::Key_hyphen, AzFramework::InputDeviceKeyboard::Key::PunctuationHyphen },
|
||||
{ Qt::Key_Period, AzFramework::InputDeviceKeyboard::Key::PunctuationPeriod },
|
||||
{ Qt::Key_Semicolon, AzFramework::InputDeviceKeyboard::Key::PunctuationSemicolon },
|
||||
{ Qt::Key_Slash, AzFramework::InputDeviceKeyboard::Key::PunctuationSlash },
|
||||
{ Qt::Key_QuoteLeft, AzFramework::InputDeviceKeyboard::Key::PunctuationTilde },
|
||||
{ Qt::Key_Pause, AzFramework::InputDeviceKeyboard::Key::WindowsSystemPause },
|
||||
{ Qt::Key_Print, AzFramework::InputDeviceKeyboard::Key::WindowsSystemPrint },
|
||||
{ Qt::Key_ScrollLock, AzFramework::InputDeviceKeyboard::Key::WindowsSystemScrollLock },
|
||||
} };
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::InitializeMouseButtonMappings()
|
||||
{
|
||||
m_mouseButtonMappings = { {
|
||||
{ Qt::MouseButton::LeftButton, AzFramework::InputDeviceMouse::Button::Left },
|
||||
{ Qt::MouseButton::RightButton, AzFramework::InputDeviceMouse::Button::Right },
|
||||
{ Qt::MouseButton::MiddleButton, AzFramework::InputDeviceMouse::Button::Middle },
|
||||
{ Qt::MouseButton::ExtraButton1, AzFramework::InputDeviceMouse::Button::Other1 },
|
||||
{ Qt::MouseButton::ExtraButton2, AzFramework::InputDeviceMouse::Button::Other2 },
|
||||
} };
|
||||
}
|
||||
|
||||
// Currently this is only set for modifier keys.
|
||||
// This should only be expanded sparingly, any keys handled here will not be bubbled up to the shortcut system.
|
||||
// ex: If Key_S was here, the viewport would consume S key presses before the application could process a QAction with a Ctrl+S
|
||||
// shortcut.
|
||||
void QtEventToAzInputMapper::InitializeHighPriorityKeys()
|
||||
{
|
||||
m_highPriorityKeys = { Qt::Key_Alt, Qt::Key_Control, Qt::Key_Shift, Qt::Key_Super_L, Qt::Key_Super_R };
|
||||
}
|
||||
|
||||
QtEventToAzInputMapper::EditorQtKeyboardDevice::EditorQtKeyboardDevice(AzFramework::InputDeviceId id)
|
||||
: AzFramework::InputDeviceKeyboard(id)
|
||||
{
|
||||
// Disable all platform native processing in favor of our Qt event handling
|
||||
SetImplementation(nullptr);
|
||||
}
|
||||
|
||||
QtEventToAzInputMapper::EditorQtMouseDevice::EditorQtMouseDevice(AzFramework::InputDeviceId id)
|
||||
: AzFramework::InputDeviceMouse(id)
|
||||
{
|
||||
// Disable all platform native processing in favor of our Qt event handling
|
||||
SetImplementation(nullptr);
|
||||
}
|
||||
|
||||
QtEventToAzInputMapper::QtEventToAzInputMapper(QWidget* sourceWidget, int syntheticDeviceId)
|
||||
: QObject(sourceWidget)
|
||||
, m_sourceWidget(sourceWidget)
|
||||
, m_keyboardModifiers(AZStd::make_shared<AzFramework::ModifierKeyStates>())
|
||||
, m_cursorPosition(AZStd::make_shared<AzFramework::InputChannel::PositionData2D>())
|
||||
{
|
||||
InitializeKeyMappings();
|
||||
InitializeMouseButtonMappings();
|
||||
InitializeHighPriorityKeys();
|
||||
|
||||
// Add an arbitrary offset to our device index to avoid collision with real physical device index.
|
||||
// We still have to use the keyboard and mouse device channel names because input channels are only addressed
|
||||
// by their own name and their device index, so overlapping input channels between devices would conflict.
|
||||
constexpr AZ::u32 syntheticDeviceOffset = 1000;
|
||||
const AzFramework::InputDeviceId keyboardDeviceId(
|
||||
AzFramework::InputDeviceKeyboard::Id.GetName(), syntheticDeviceId + syntheticDeviceOffset);
|
||||
const AzFramework::InputDeviceId mouseDeviceId(
|
||||
AzFramework::InputDeviceMouse::Id.GetName(), syntheticDeviceId + syntheticDeviceOffset);
|
||||
|
||||
m_keyboardDevice = AZStd::make_unique<EditorQtKeyboardDevice>(keyboardDeviceId);
|
||||
m_mouseDevice = AZStd::make_unique<EditorQtMouseDevice>(mouseDeviceId);
|
||||
|
||||
AddChannels(m_keyboardDevice->m_allChannelsById);
|
||||
AddChannels(m_mouseDevice->m_allChannelsById);
|
||||
|
||||
// Install a global event filter to ensure we don't miss mouse and key release events.
|
||||
QApplication::instance()->installEventFilter(this);
|
||||
}
|
||||
|
||||
bool QtEventToAzInputMapper::HandlesInputEvent(const AzFramework::InputChannel& channel) const
|
||||
{
|
||||
const AzFramework::InputChannelId& channelId = channel.GetInputChannelId();
|
||||
if (channelId == AzFramework::InputDeviceMouse::Movement::X || channelId == AzFramework::InputDeviceMouse::Movement::Y)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// We map keyboard and mouse events from Qt, so flag all events coming from those devices
|
||||
// as handled by our synthetic event system.
|
||||
const AzFramework::InputDeviceId& deviceId = channel.GetInputDevice().GetInputDeviceId();
|
||||
return deviceId.GetNameCrc32() == AzFramework::InputDeviceMouse::Id.GetNameCrc32() ||
|
||||
deviceId.GetNameCrc32() == AzFramework::InputDeviceKeyboard::Id.GetNameCrc32();
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::SetEnabled(bool enabled)
|
||||
{
|
||||
m_enabled = enabled;
|
||||
if (!enabled)
|
||||
{
|
||||
// Send an internal focus change event to reset our input state to fresh if we're disabled.
|
||||
HandleFocusChange(nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool QtEventToAzInputMapper::eventFilter(QObject* object, QEvent* event)
|
||||
{
|
||||
// Abort if processing isn't enabled.
|
||||
if (!m_enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Because there's no "end" to mouse movement and wheel events, we reset mouse movement channels that have been opened
|
||||
// during the next processed non-mouse event.
|
||||
if (m_mouseChannelsNeedUpdate && event->type() != QEvent::Type::MouseMove && event->type() != QEvent::Type::Wheel)
|
||||
{
|
||||
m_cursorPosition->m_normalizedPositionDelta = AZ::Vector2::CreateZero();
|
||||
ProcessPendingMouseEvents();
|
||||
m_mouseChannelsNeedUpdate = false;
|
||||
}
|
||||
|
||||
// Only accept mouse & key release events that originate from an object that is not our target widget,
|
||||
// as we don't want to erroneously intercept user input meant for another component.
|
||||
if (object != m_sourceWidget && event->type() != QEvent::Type::KeyRelease && event->type() != QEvent::Type::MouseButtonRelease)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If our focus changes, go ahead and reset all input devices.
|
||||
if (event->type() == QEvent::FocusIn || event->type() == QEvent::FocusOut)
|
||||
{
|
||||
HandleFocusChange(event);
|
||||
}
|
||||
// Map key events to input channels.
|
||||
// ShortcutOverride is used in lieu of KeyPress for high priority input channels like Alt
|
||||
// that need to be accepted and stopped before they bubble up and cause unintended behavior.
|
||||
else if (
|
||||
event->type() == QEvent::Type::KeyPress || event->type() == QEvent::Type::KeyRelease ||
|
||||
event->type() == QEvent::Type::ShortcutOverride)
|
||||
{
|
||||
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
|
||||
HandleKeyEvent(keyEvent);
|
||||
}
|
||||
// Map mouse events to input channels.
|
||||
else if (event->type() == QEvent::Type::MouseButtonPress || event->type() == QEvent::Type::MouseButtonRelease || event->type() == QEvent::Type::MouseButtonDblClick)
|
||||
{
|
||||
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
|
||||
HandleMouseButtonEvent(mouseEvent);
|
||||
}
|
||||
// Map mouse movement to the movement input channels.
|
||||
// This includes SystemCursorPosition alongside Movement::X and Movement::Y.
|
||||
else if (event->type() == QEvent::Type::MouseMove)
|
||||
{
|
||||
QMouseEvent* mouseEvent = static_cast<QMouseEvent*>(event);
|
||||
HandleMouseMoveEvent(mouseEvent);
|
||||
}
|
||||
// Map wheel events to the mouse Z movement channel.
|
||||
else if (event->type() == QEvent::Type::Wheel)
|
||||
{
|
||||
QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event);
|
||||
HandleWheelEvent(wheelEvent);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::NotifyUpdateChannelIfNotIdle(const AzFramework::InputChannel* channel, QEvent* event)
|
||||
{
|
||||
if (channel->GetState() != AzFramework::InputChannel::State::Idle)
|
||||
{
|
||||
emit InputChannelUpdated(channel, event);
|
||||
}
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::ProcessPendingMouseEvents()
|
||||
{
|
||||
auto systemCursorChannel =
|
||||
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::SystemCursorPosition);
|
||||
auto mouseWheelChannel =
|
||||
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::Movement::Z);
|
||||
|
||||
systemCursorChannel->ProcessRawInputEvent(m_cursorPosition->m_normalizedPositionDelta.GetLength());
|
||||
mouseWheelChannel->ProcessRawInputEvent(0.f);
|
||||
|
||||
NotifyUpdateChannelIfNotIdle(systemCursorChannel, nullptr);
|
||||
NotifyUpdateChannelIfNotIdle(mouseWheelChannel, nullptr);
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::HandleMouseButtonEvent(QMouseEvent* mouseEvent)
|
||||
{
|
||||
const Qt::MouseButton button = mouseEvent->button();
|
||||
|
||||
if (auto buttonIt = m_mouseButtonMappings.find(button); buttonIt != m_mouseButtonMappings.end())
|
||||
{
|
||||
auto buttonChannel = GetInputChannel<AzFramework::InputChannelDigitalWithSharedPosition2D>(buttonIt->second);
|
||||
|
||||
if (buttonChannel)
|
||||
{
|
||||
if (mouseEvent->type() != QEvent::Type::MouseButtonRelease)
|
||||
{
|
||||
buttonChannel->UpdateState(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
buttonChannel->UpdateState(false);
|
||||
}
|
||||
|
||||
NotifyUpdateChannelIfNotIdle(buttonChannel, mouseEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::HandleMouseMoveEvent(QMouseEvent* mouseEvent)
|
||||
{
|
||||
const QPoint mousePos = mouseEvent->pos();
|
||||
const float normalizedX = aznumeric_cast<float>(mousePos.x()) / aznumeric_cast<float>(m_sourceWidget->width());
|
||||
const float normalizedY = aznumeric_cast<float>(mousePos.y()) / aznumeric_cast<float>(m_sourceWidget->height());
|
||||
const AZ::Vector2 normalizedPosition(normalizedX, normalizedY);
|
||||
m_cursorPosition->m_normalizedPositionDelta = normalizedPosition - m_cursorPosition->m_normalizedPosition;
|
||||
m_cursorPosition->m_normalizedPosition = normalizedPosition;
|
||||
ProcessPendingMouseEvents();
|
||||
m_mouseChannelsNeedUpdate = true;
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::HandleKeyEvent(QKeyEvent* keyEvent)
|
||||
{
|
||||
// Ignore key repeat events, they're unrelated to actual physical button presses.
|
||||
if (keyEvent->isAutoRepeat())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
const Qt::Key key = static_cast<Qt::Key>(keyEvent->key());
|
||||
|
||||
// For ShortcutEvent, only continue processing if we're in the HighPriorityKeys set.
|
||||
if (keyEvent->type() != QEvent::Type::ShortcutOverride || m_highPriorityKeys.find(key) != m_highPriorityKeys.end())
|
||||
{
|
||||
if (auto keyIt = m_keyMappings.find(key); keyIt != m_keyMappings.end())
|
||||
{
|
||||
auto keyChannel = GetInputChannel<AzFramework::InputChannelDigitalWithSharedModifierKeyStates>(keyIt->second);
|
||||
|
||||
if (keyChannel)
|
||||
{
|
||||
if (keyEvent->type() == QEvent::Type::KeyPress || keyEvent->type() == QEvent::Type::ShortcutOverride)
|
||||
{
|
||||
keyChannel->UpdateState(true);
|
||||
}
|
||||
else
|
||||
{
|
||||
keyChannel->UpdateState(false);
|
||||
}
|
||||
|
||||
NotifyUpdateChannelIfNotIdle(keyChannel, keyEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::HandleWheelEvent(QWheelEvent* wheelEvent)
|
||||
{
|
||||
auto cursorZChannel =
|
||||
GetInputChannel<AzFramework::InputChannelDeltaWithSharedPosition2D>(AzFramework::InputDeviceMouse::Movement::Z);
|
||||
const QPoint angleDelta = wheelEvent->angleDelta();
|
||||
// Check both angles, as the alt modifier can change the wheel direction.
|
||||
int wheelAngle = angleDelta.x();
|
||||
if (wheelAngle == 0)
|
||||
{
|
||||
wheelAngle = angleDelta.y();
|
||||
}
|
||||
cursorZChannel->ProcessRawInputEvent(aznumeric_cast<float>(wheelAngle));
|
||||
NotifyUpdateChannelIfNotIdle(cursorZChannel, wheelEvent);
|
||||
m_mouseChannelsNeedUpdate = true;
|
||||
}
|
||||
|
||||
void QtEventToAzInputMapper::HandleFocusChange(QEvent* event)
|
||||
{
|
||||
for (auto& channelData : m_channels)
|
||||
{
|
||||
// If resetting the input device changed the channel state, submit it to the mapped channel list
|
||||
// for processing.
|
||||
if (channelData.second->IsActive())
|
||||
{
|
||||
channelData.second->UpdateState(false);
|
||||
NotifyUpdateChannelIfNotIdle(channelData.second, event);
|
||||
}
|
||||
}
|
||||
m_mouseChannelsNeedUpdate = false;
|
||||
}
|
||||
} // namespace AzToolsFramework
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 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
|
||||
*
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#if !defined(Q_MOC_RUN)
|
||||
#include <AzCore/std/smart_ptr/shared_ptr.h>
|
||||
#include <AzCore/std/smart_ptr/unique_ptr.h>
|
||||
#include <AzFramework/Input/Channels/InputChannel.h>
|
||||
#include <AzFramework/Input/Channels/InputChannelDeltaWithSharedPosition2D.h>
|
||||
#include <AzFramework/Input/Channels/InputChannelDigitalWithSharedModifierKeyStates.h>
|
||||
#include <AzFramework/Input/Channels/InputChannelDigitalWithSharedPosition2D.h>
|
||||
|
||||
#include <AzFramework/Input/Devices/Keyboard/InputDeviceKeyboard.h>
|
||||
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
|
||||
|
||||
#include <QEvent>
|
||||
#include <QObject>
|
||||
#endif //! defined(Q_MOC_RUN)
|
||||
|
||||
class QWidget;
|
||||
class QKeyEvent;
|
||||
class QMouseEvent;
|
||||
class QWheelEvent;
|
||||
|
||||
namespace AzToolsFramework
|
||||
{
|
||||
//! Maps events from the Qt input system to synthetic InputChannels in AzFramework
|
||||
//! that can be used by AzFramework::ViewportControllers.
|
||||
class QtEventToAzInputMapper final : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
QtEventToAzInputMapper(QWidget* sourceWidget, int syntheticDeviceId = 0);
|
||||
~QtEventToAzInputMapper() = default;
|
||||
|
||||
//! Queries whether a given input channel has a synthetic equivalent mapped
|
||||
//! by this system.
|
||||
//! \returns true if the channel is handled by MapQtEventToAzInput.
|
||||
bool HandlesInputEvent(const AzFramework::InputChannel& channel) const;
|
||||
|
||||
//! Sets whether or not this input mapper should be updating its input channels from Qt events.
|
||||
void SetEnabled(bool enabled);
|
||||
|
||||
// QObject overrides...
|
||||
bool eventFilter(QObject* object, QEvent* event) override;
|
||||
|
||||
signals:
|
||||
//! This signal fires whenever the state of the specified input channel changes.
|
||||
//! This is determined by Qt events dispatched to the source widget.
|
||||
//! \param channel The AZ input channel that has been updated.
|
||||
//! \param event The underlying Qt event that triggered this change, if applicable.
|
||||
void InputChannelUpdated(const AzFramework::InputChannel* channel, QEvent* event);
|
||||
|
||||
private:
|
||||
// Gets an input channel of the specified type by ID.
|
||||
template<class TInputChannel>
|
||||
TInputChannel* GetInputChannel(const AzFramework::InputChannelId& id)
|
||||
{
|
||||
auto channelIt = m_channels.find(id);
|
||||
if (channelIt != m_channels.end())
|
||||
{
|
||||
return static_cast<TInputChannel*>(channelIt->second);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Adds channels from the specified channel container to our input channel ID -> input channel lookup table.
|
||||
// Used for rapid lookup.
|
||||
template <class TContainer>
|
||||
void AddChannels(const TContainer& container)
|
||||
{
|
||||
for (const auto& channelData : container)
|
||||
{
|
||||
// Break const as we're taking these input channels from devices we own.
|
||||
m_channels.emplace(channelData.first, const_cast<AzFramework::InputChannel*>(channelData.second));
|
||||
}
|
||||
}
|
||||
|
||||
// Our synthetic Keyboard device, does no internal keyboard handling and instead listens to this class for updates.
|
||||
class EditorQtKeyboardDevice : public AzFramework::InputDeviceKeyboard
|
||||
{
|
||||
public:
|
||||
EditorQtKeyboardDevice(AzFramework::InputDeviceId id);
|
||||
|
||||
friend class QtEventToAzInputMapper;
|
||||
};
|
||||
|
||||
// Our synthetic Mouse device, does no internal keyboard handling and instead listens to this class for updates.
|
||||
class EditorQtMouseDevice : public AzFramework::InputDeviceMouse
|
||||
{
|
||||
public:
|
||||
EditorQtMouseDevice(AzFramework::InputDeviceId id);
|
||||
|
||||
friend class QtEventToAzInputMapper;
|
||||
};
|
||||
|
||||
// Emits InputChannelUpdated if channel has transitioned in state (i.e. has gone from active to inactive or vice versa).
|
||||
void NotifyUpdateChannelIfNotIdle(const AzFramework::InputChannel* channel, QEvent* event);
|
||||
|
||||
// Processes any pending mouse movement events, this allows mouse movement channels to close themselves.
|
||||
void ProcessPendingMouseEvents();
|
||||
|
||||
// Handle mouse click events.
|
||||
void HandleMouseButtonEvent(QMouseEvent* mouseEvent);
|
||||
// Handle mouse move events.
|
||||
void HandleMouseMoveEvent(QMouseEvent* mouseEvent);
|
||||
// Handles key press / release events (or ShortcutOverride events for keys listed in m_highPriorityKeys).
|
||||
void HandleKeyEvent(QKeyEvent* keyEvent);
|
||||
// Handles mouse wheel events.
|
||||
void HandleWheelEvent(QWheelEvent* wheelEvent);
|
||||
// Handles focus change events.
|
||||
void HandleFocusChange(QEvent* event);
|
||||
|
||||
// Populates m_keyMappings.
|
||||
void InitializeKeyMappings();
|
||||
// Populates m_mouseButtonMappings.
|
||||
void InitializeMouseButtonMappings();
|
||||
// Populates m_highPriorityKeys.
|
||||
void InitializeHighPriorityKeys();
|
||||
|
||||
// The current keyboard modifier state used by our synthetic key input channels.
|
||||
AZStd::shared_ptr<AzFramework::ModifierKeyStates> m_keyboardModifiers;
|
||||
// The current normalized cursor position used by our synthetic system cursor event.
|
||||
AZStd::shared_ptr<AzFramework::InputChannel::PositionData2D> m_cursorPosition;
|
||||
// A lookup table for Qt key -> AZ input channel.
|
||||
AZStd::unordered_map<Qt::Key, AzFramework::InputChannelId> m_keyMappings;
|
||||
// A lookup table for Qt mouse button -> AZ input channel.
|
||||
AZStd::unordered_map<Qt::MouseButton, AzFramework::InputChannelId> m_mouseButtonMappings;
|
||||
// A set of high priority keys that need to be processed at the ShortcutOverride level instead of the
|
||||
// KeyEvent level. This prevents e.g. the main menu bar from processing a press of the "alt" key when the
|
||||
// viewport consumes the event.
|
||||
AZStd::unordered_set<Qt::Key> m_highPriorityKeys;
|
||||
// A lookup table for AZ input channel ID -> physical input channel on our mouse or keyboard device.
|
||||
AZStd::unordered_map<AzFramework::InputChannelId, AzFramework::InputChannel*> m_channels;
|
||||
// The source widget to map events from, used to calculate the relative mouse position within the widget bounds.
|
||||
QWidget* m_sourceWidget;
|
||||
// Flags when mouse movement channels have been opened and may need to be closed (as there are no movement ended events).
|
||||
bool m_mouseChannelsNeedUpdate = false;
|
||||
// Flags whether or not Qt events should currently be processed.
|
||||
bool m_enabled = true;
|
||||
|
||||
// Our viewport-specific AZ devices. We control their internal input channel states.
|
||||
AZStd::unique_ptr<EditorQtMouseDevice> m_mouseDevice;
|
||||
AZStd::unique_ptr<EditorQtKeyboardDevice> m_keyboardDevice;
|
||||
};
|
||||
} // namespace AzToolsFramework
|
||||
@@ -724,6 +724,8 @@ set(FILES
|
||||
PythonTerminal/ScriptTermDialog.cpp
|
||||
PythonTerminal/ScriptTermDialog.h
|
||||
PythonTerminal/ScriptTermDialog.ui
|
||||
Input/QtEventToAzInputManager.h
|
||||
Input/QtEventToAzInputManager.cpp
|
||||
)
|
||||
|
||||
# Prevent the following files from being grouped in UNITY builds
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#include <AzCore/Math/Vector2.h>
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
|
||||
@@ -229,6 +229,7 @@ namespace AZ
|
||||
{
|
||||
m_clearContainers = false;
|
||||
|
||||
m_stackLevel = 0;
|
||||
m_cachedTimeRegionMap.clear();
|
||||
m_timeRegionStack.clear();
|
||||
m_cachedTimeRegions.clear();
|
||||
|
||||
+9
-1
@@ -11,6 +11,7 @@
|
||||
#include <QElapsedTimer>
|
||||
#include <Atom/RPI.Public/Base.h>
|
||||
#include <AzToolsFramework/Viewport/ViewportMessages.h>
|
||||
#include <AzToolsFramework/Input/QtEventToAzInputManager.h>
|
||||
#include <AzFramework/Input/Events/InputChannelEventListener.h>
|
||||
#include <AzFramework/Scene/Scene.h>
|
||||
#include <AzFramework/Viewport/ViewportControllerInterface.h>
|
||||
@@ -81,6 +82,12 @@ namespace AtomToolsFramework
|
||||
//! Gets the default camera that's been automatically registered to our ViewportContext.
|
||||
AZ::RPI::ViewPtr GetDefaultCamera();
|
||||
AZ::RPI::ConstViewPtr GetDefaultCamera() const;
|
||||
//! Sets whether or not input processing is enabled for this RenderViewportWidget.
|
||||
//! While input processing is enabled, synthetic input events may appear in OnInputChannelEventFiltered
|
||||
//! due to internal viewport input mapping via QtEventToAzInputMapper, so it may be desirable to disable
|
||||
//! camera controller input processing wholesale to avoid competing input messages.
|
||||
//! Input processing is enabled by default.
|
||||
void SetInputProcessingEnabled(bool enabled);
|
||||
|
||||
// AzToolsFramework::ViewportInteraction::ViewportInteractionRequestBus::Handler ...
|
||||
AzFramework::CameraState GetCameraState() override;
|
||||
@@ -130,7 +137,6 @@ namespace AtomToolsFramework
|
||||
|
||||
private:
|
||||
void SendWindowResizeEvent();
|
||||
bool CanInputGrantFocus(const AzFramework::InputChannel& inputChannel) const;
|
||||
|
||||
// The underlying ViewportContext, our entry-point to the Atom RPI.
|
||||
AZ::RPI::ViewportContextPtr m_viewportContext;
|
||||
@@ -157,5 +163,7 @@ namespace AtomToolsFramework
|
||||
AZStd::optional<QPoint> m_lastCursorPosition;
|
||||
// The viewport settings (e.g. grid snapping, grid size) for this viewport.
|
||||
const AzToolsFramework::ViewportInteraction::ViewportSettings* m_viewportSettings = nullptr;
|
||||
// Maps our internal Qt events into AzFramework InputChannels for our ViewportControllerList.
|
||||
AzToolsFramework::QtEventToAzInputMapper* m_inputChannelMapper = nullptr;
|
||||
};
|
||||
} //namespace AtomToolsFramework
|
||||
|
||||
@@ -81,6 +81,23 @@ namespace AtomToolsFramework
|
||||
AZ::TickBus::Handler::BusConnect();
|
||||
AzFramework::WindowRequestBus::Handler::BusConnect(params.windowHandle);
|
||||
|
||||
m_inputChannelMapper = new AzToolsFramework::QtEventToAzInputMapper(this, id);
|
||||
|
||||
// Forward input events to our controller list.
|
||||
QObject::connect(m_inputChannelMapper, &AzToolsFramework::QtEventToAzInputMapper::InputChannelUpdated, this,
|
||||
[this](const AzFramework::InputChannel* inputChannel, QEvent* event)
|
||||
{
|
||||
AzFramework::NativeWindowHandle windowId = reinterpret_cast<AzFramework::NativeWindowHandle>(winId());
|
||||
if (m_controllerList->HandleInputChannelEvent({GetId(), windowId, *inputChannel}))
|
||||
{
|
||||
// If the controller handled the input event, mark the event as accepted so it doesn't continue to propagate.
|
||||
if (event)
|
||||
{
|
||||
event->setAccepted(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -149,86 +166,24 @@ namespace AtomToolsFramework
|
||||
return m_defaultCamera;
|
||||
}
|
||||
|
||||
static bool IsMouseButtonEvent(const AzFramework::InputChannel& inputChannel)
|
||||
{
|
||||
const auto& mouseButtons = AzFramework::InputDeviceMouse::Button::All;
|
||||
return AZStd::find(mouseButtons.begin(), mouseButtons.end(), inputChannel.GetInputChannelId()) != mouseButtons.end();
|
||||
}
|
||||
|
||||
static bool IsMouseMoveEvent(const AzFramework::InputChannel& inputChannel)
|
||||
{
|
||||
return inputChannel.GetInputChannelId() == AzFramework::InputDeviceMouse::SystemCursorPosition;
|
||||
}
|
||||
|
||||
static bool IsMouseButtonOrWheelEvent(const AzFramework::InputChannel& inputChannel)
|
||||
{
|
||||
return IsMouseButtonEvent(inputChannel) || inputChannel.GetInputChannelId() == AzFramework::InputDeviceMouse::Movement::Z;
|
||||
}
|
||||
|
||||
bool RenderViewportWidget::CanInputGrantFocus(const AzFramework::InputChannel& inputChannel) const
|
||||
{
|
||||
// Only take focus from a mouse event if the cursor is currently within the viewport
|
||||
if (!m_mouseOver)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only mouse button down events (clicks) can grant focus
|
||||
if (inputChannel.GetState() != AzFramework::InputChannel::State::Began)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only mouse button events can grant focus
|
||||
return IsMouseButtonEvent(inputChannel);
|
||||
}
|
||||
|
||||
bool RenderViewportWidget::OnInputChannelEventFiltered(const AzFramework::InputChannel& inputChannel)
|
||||
{
|
||||
bool shouldConsumeEvent = true;
|
||||
|
||||
// Grab keyboard focus if we've been clicked on.
|
||||
// Qt normally handles this for us, but we're filtering native events before they get
|
||||
// synthesized into QMouseEvents.
|
||||
if (!hasFocus() && CanInputGrantFocus(inputChannel))
|
||||
{
|
||||
setFocus();
|
||||
}
|
||||
|
||||
// Don't consume new input events if we don't currently have focus.
|
||||
// We do forward Ended events, as they may be relevant to our current state
|
||||
// (e.g. a key gets released after we lose focus, it shouldn't remain "stuck").
|
||||
if (!hasFocus())
|
||||
{
|
||||
if (inputChannel.GetState() == AzFramework::InputChannel::State::Ended)
|
||||
{
|
||||
// Forward the input ended event to our controllers, but don't prevent other viewports from receiving it.
|
||||
shouldConsumeEvent = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Not an event we should listen to, abort.
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// If we receive a mouse button event from outside of our viewport, ignore it even if we have focus.
|
||||
if (!m_mouseOver
|
||||
&& inputChannel.GetState() == AzFramework::InputChannel::State::Began
|
||||
&& IsMouseButtonOrWheelEvent(inputChannel))
|
||||
// Only forward channels that aren't covered by our Qt -> AZ event mapper
|
||||
if (!m_inputChannelMapper || m_inputChannelMapper->HandlesInputEvent(inputChannel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Don't forward system cursor position updates, we'll do that ourselves for in-window movements once the result of
|
||||
// ViewportCursorScreenPosition is guaranteed to be correct (see mouseMoveEvent).
|
||||
if (IsMouseMoveEvent(inputChannel))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool shouldConsumeEvent = true;
|
||||
|
||||
AzFramework::NativeWindowHandle windowId = reinterpret_cast<AzFramework::NativeWindowHandle>(winId());
|
||||
const bool eventHandled = m_controllerList->HandleInputChannelEvent({GetId(), windowId, inputChannel});
|
||||
|
||||
// If our controllers handled the event and it's one we can safely consume (i.e. it's not an Ended event that other viewports might need), consume it.
|
||||
return eventHandled && shouldConsumeEvent;
|
||||
}
|
||||
@@ -262,6 +217,7 @@ namespace AtomToolsFramework
|
||||
{
|
||||
SendWindowResizeEvent();
|
||||
}
|
||||
|
||||
return QWidget::event(event);
|
||||
}
|
||||
|
||||
@@ -279,23 +235,12 @@ namespace AtomToolsFramework
|
||||
{
|
||||
m_mousePosition = event->localPos();
|
||||
|
||||
// Now that we've looked a viewport local mouse position,
|
||||
// we can go ahead and broadcast the system cursor input event to the controllers.
|
||||
// This allows any controllers not listening to pure mouse deltas to consistently
|
||||
// look up the mouse position in viewport screen coordinates.
|
||||
const AzFramework::InputDevice* mouseInputDevice = nullptr;
|
||||
if (AzFramework::InputDeviceRequestBus::EventResult(
|
||||
mouseInputDevice, AzFramework::InputDeviceMouse::Id, &AzFramework::InputDeviceRequests::GetInputDevice);
|
||||
mouseInputDevice != nullptr)
|
||||
{
|
||||
const AzFramework::NativeWindowHandle windowId = reinterpret_cast<AzFramework::NativeWindowHandle>(winId());
|
||||
AzFramework::InputChannel syntheticInput(AzFramework::InputDeviceMouse::SystemCursorPosition, *mouseInputDevice);
|
||||
m_controllerList->HandleInputChannelEvent({GetId(), windowId, syntheticInput});
|
||||
}
|
||||
|
||||
if (m_capturingCursor && m_lastCursorPosition.has_value())
|
||||
{
|
||||
AzQtComponents::SetCursorPos(m_lastCursorPosition.value());
|
||||
// Even though we just set the cursor position, there are edge cases such as remote desktop that will leave
|
||||
// the cursor position unchanged. For safety, we re-cache our last cursor position for delta generation.
|
||||
m_lastCursorPosition = QCursor::pos();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -354,6 +299,12 @@ namespace AtomToolsFramework
|
||||
}
|
||||
}
|
||||
|
||||
void RenderViewportWidget::SetInputProcessingEnabled(bool enabled)
|
||||
{
|
||||
m_inputChannelMapper->SetEnabled(enabled);
|
||||
m_controllerList->SetEnabled(enabled);
|
||||
}
|
||||
|
||||
AzFramework::CameraState RenderViewportWidget::GetCameraState()
|
||||
{
|
||||
AZ::RPI::ViewPtr currentView = m_viewportContext->GetDefaultView();
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
#include <AzCore/Component/TickBus.h>
|
||||
#include <AzCore/Math/Random.h>
|
||||
|
||||
#include <Atom/RHI.Reflect/CpuTimingStatistics.h>
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
@@ -19,24 +23,41 @@ namespace AZ
|
||||
|
||||
namespace Render
|
||||
{
|
||||
struct ThreadRegionEntry
|
||||
struct ThreadRegionEntry
|
||||
{
|
||||
AZStd::thread_id m_threadId;
|
||||
AZStd::sys_time_t m_startTick = 0;
|
||||
AZStd::sys_time_t m_endTick = 0;
|
||||
};
|
||||
|
||||
// Stores data about a region that is agreggated from all collected frames
|
||||
// Data collection can be toggled on and off through m_record.
|
||||
struct RegionStatistics
|
||||
{
|
||||
float CalcAverageTimeMs() const;
|
||||
void RecordRegion(const AZ::RHI::CachedTimeRegion& region);
|
||||
|
||||
bool m_draw = false;
|
||||
bool m_record = true;
|
||||
u64 m_invocations = 0;
|
||||
AZStd::sys_time_t m_totalTicks = 0;
|
||||
};
|
||||
|
||||
//! Visual profiler for Cpu statistics.
|
||||
//! It uses ImGui as the library for displaying the Attachments and Heaps.
|
||||
//! It shows all heaps that are being used by the RHI and how the
|
||||
//! resources are allocated in each heap.
|
||||
class ImGuiCpuProfiler
|
||||
: SystemTickBus::Handler
|
||||
{
|
||||
// Region Name -> Array of ThreadRegion entries
|
||||
using RegionEntryMap = AZStd::map<AZStd::string, AZStd::vector<ThreadRegionEntry>>;
|
||||
// Group Name -> RegionEntryMap
|
||||
using GroupRegionMap = AZStd::map<AZStd::string, RegionEntryMap>;
|
||||
|
||||
using TimeRegion = AZ::RHI::CachedTimeRegion;
|
||||
using GroupRegionName = AZ::RHI::CachedTimeRegion::GroupRegionName;
|
||||
|
||||
public:
|
||||
ImGuiCpuProfiler() = default;
|
||||
~ImGuiCpuProfiler() = default;
|
||||
@@ -44,7 +65,13 @@ namespace AZ
|
||||
//! Draws the provided Cpu statistics.
|
||||
void Draw(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& cpuTimingStatistics);
|
||||
|
||||
//! Draws the CPU profiling visualizer in a new window.
|
||||
void DrawVisualizer(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics);
|
||||
|
||||
private:
|
||||
static constexpr float RowHeight = 50.0;
|
||||
static constexpr int DefaultFramesToCollect = 50;
|
||||
|
||||
// Update the GroupRegionMap with the latest cached time regions
|
||||
void UpdateGroupRegionMap();
|
||||
|
||||
@@ -62,8 +89,73 @@ namespace AZ
|
||||
AZ::RHI::CpuTimingStatistics m_cpuTimingStatisticsWhenPause;
|
||||
|
||||
AZStd::string m_lastCapturedFilePath;
|
||||
|
||||
// Visualizer methods
|
||||
|
||||
// Get the profiling data from the last frame, only called when the profiler is not paused.
|
||||
void CollectFrameData();
|
||||
|
||||
// Cull old data from internal storage, only called when profiler is not paused.
|
||||
void CullFrameData(const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics);
|
||||
|
||||
// Draws a single block onto the timeline
|
||||
void DrawBlock(const TimeRegion& block, u64 targetRow);
|
||||
|
||||
// Draw horizontal lines between threads in the timeline
|
||||
void DrawThreadSeparator(u64 threadBoundary, u64 maxDepth);
|
||||
|
||||
// Draw the "Thread XXXXX" label onto the viewport
|
||||
void DrawThreadLabel(u64 baseRow, AZStd::thread_id threadId);
|
||||
|
||||
// Draws all active function statistics windows
|
||||
void DrawRegionStatistics();
|
||||
|
||||
// Draw the vertical lines separating frames in the timeline
|
||||
void DrawFrameBoundaries();
|
||||
|
||||
// Draw the ruler with frame time labels
|
||||
void DrawRuler();
|
||||
|
||||
// Converts raw ticks to a pixel value suitable to give to ImDrawList, handles window scrolling
|
||||
float ConvertTickToPixelSpace(AZStd::sys_time_t tick) const;
|
||||
|
||||
AZStd::sys_time_t GetViewportTickWidth() const;
|
||||
|
||||
// Gets the color for a block using the GroupRegionName as a key into the cache
|
||||
// Generates a random ImU32 if the block does not yet have a color
|
||||
ImU32 GetBlockColor(const TimeRegion& block);
|
||||
|
||||
// System tick bus overrides
|
||||
virtual void OnSystemTick() override;
|
||||
|
||||
// Visualizer state
|
||||
|
||||
bool m_showVisualizer = false;
|
||||
|
||||
int m_framesToCollect = DefaultFramesToCollect;
|
||||
|
||||
// Tally of the number of saved profiling events so far
|
||||
u64 m_savedRegionCount = 0;
|
||||
|
||||
// Viewport tick bounds, these are used to convert tick space -> screen space and cull so we only draw onscreen objects
|
||||
AZStd::sys_time_t m_viewportStartTick;
|
||||
AZStd::sys_time_t m_viewportEndTick;
|
||||
|
||||
// Map to store each thread's TimeRegions, individual vectors are sorted by start tick
|
||||
AZStd::unordered_map<AZStd::thread_id, AZStd::vector<TimeRegion>> m_savedData;
|
||||
|
||||
// Region color cache
|
||||
AZStd::unordered_map<const GroupRegionName*, ImVec4> m_regionColorMap;
|
||||
|
||||
// Tracks the frame boundaries
|
||||
AZStd::vector<AZStd::sys_time_t> m_frameEndTicks = { INT64_MIN };
|
||||
|
||||
// Main data structure for storing function statistics to be shown in the popup windows.
|
||||
// For now we default allocate for all regions on the first render frame and then use RegionStatistics.m_draw to determine
|
||||
// if we should draw the window or not. FIXME(ATOM-15948) this should be changed once RegionStatistics gets heavier.
|
||||
AZStd::unordered_map<const GroupRegionName*, RegionStatistics> m_regionStatisticsMap;
|
||||
};
|
||||
} // namespace Render
|
||||
}
|
||||
} // namespace AZ
|
||||
|
||||
#include "ImGuiCpuProfiler.inl"
|
||||
|
||||
@@ -6,10 +6,16 @@
|
||||
*/
|
||||
|
||||
#include <Atom/Feature/Utils/ProfilingCaptureBus.h>
|
||||
#include <Atom/RHI.Reflect/CpuTimingStatistics.h>
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
#include <Atom/RPI.Public/RPISystemInterface.h>
|
||||
|
||||
#include <AzCore/IO/Path/Path_fwd.h>
|
||||
#include <AzCore/std/time.h>
|
||||
#include <AzCore/std/containers/map.h>
|
||||
#include <AzCore/std/containers/set.h>
|
||||
#include <AzCore/std/sort.h>
|
||||
#include <AzCore/std/time.h>
|
||||
|
||||
|
||||
namespace AZ
|
||||
{
|
||||
@@ -38,14 +44,14 @@ namespace AZ
|
||||
AZ_Assert(ticksPerSecond >= 1000, "Error in converting ticks to ms, expected ticksPerSecond >= 1000");
|
||||
return static_cast<float>((ticks * 1000) / (ticksPerSecond / 1000)) / 1000.0f;
|
||||
}
|
||||
}
|
||||
} // namespace CpuProfilerImGuiHelper
|
||||
|
||||
inline void ImGuiCpuProfiler::Draw(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics)
|
||||
{
|
||||
// Cache the value to detect if it was changed by ImGui(user pressed 'x')
|
||||
const bool cachedShowCpuProfiler = keepDrawing;
|
||||
|
||||
const ImVec2 windowSize(640.0f, 480.0f);
|
||||
const ImVec2 windowSize(900.0f, 600.0f);
|
||||
ImGui::SetNextWindowSize(windowSize, ImGuiCond_Once);
|
||||
bool captureToFile = false;
|
||||
if (ImGui::Begin("Cpu Profiler", &keepDrawing, ImGuiWindowFlags_None))
|
||||
@@ -114,9 +120,9 @@ namespace AZ
|
||||
}
|
||||
};
|
||||
|
||||
const auto ShowRegionRow = [ticksPerSecond, &DrawRegionHoverMarker, &ShowTimeInMs](const char* regionLabel,
|
||||
AZStd::vector<ThreadRegionEntry> regions,
|
||||
AZStd::sys_time_t duration)
|
||||
const auto ShowRegionRow =
|
||||
[ticksPerSecond, &DrawRegionHoverMarker,
|
||||
&ShowTimeInMs](const char* regionLabel, AZStd::vector<ThreadRegionEntry> regions, AZStd::sys_time_t duration)
|
||||
{
|
||||
// Draw the region label
|
||||
ImGui::Text(regionLabel);
|
||||
@@ -142,14 +148,15 @@ namespace AZ
|
||||
ImGui::NextColumn();
|
||||
|
||||
// Draw the time labels (max and then total)
|
||||
const AZStd::string timeLabel =
|
||||
AZStd::string::format("%.2f ms max, %.2f ms total",
|
||||
CpuProfilerImGuiHelper::TicksToMs(duration),
|
||||
const AZStd::string timeLabel = AZStd::string::format(
|
||||
"%.2f ms max, %.2f ms total", CpuProfilerImGuiHelper::TicksToMs(duration),
|
||||
CpuProfilerImGuiHelper::TicksToMs(totalTime));
|
||||
ImGui::Text(timeLabel.c_str());
|
||||
ImGui::NextColumn();
|
||||
};
|
||||
|
||||
ImGui::Checkbox("Enable Visualizer", &m_showVisualizer);
|
||||
|
||||
// Set column settings.
|
||||
ImGui::Columns(2, "view", false);
|
||||
ImGui::SetColumnWidth(0, 660.0f);
|
||||
@@ -216,8 +223,8 @@ namespace AZ
|
||||
char resolvedPath[AZ::IO::MaxPathLength];
|
||||
AZ::IO::FileIOBase::GetInstance()->ResolvePath(frameDataFilePath.c_str(), resolvedPath, AZ::IO::MaxPathLength);
|
||||
m_lastCapturedFilePath = resolvedPath;
|
||||
AZ::Render::ProfilingCaptureRequestBus::Broadcast(&AZ::Render::ProfilingCaptureRequestBus::Events::CaptureCpuProfilingStatistics,
|
||||
frameDataFilePath);
|
||||
AZ::Render::ProfilingCaptureRequestBus::Broadcast(
|
||||
&AZ::Render::ProfilingCaptureRequestBus::Events::CaptureCpuProfilingStatistics, frameDataFilePath);
|
||||
}
|
||||
|
||||
// Toggle if the bool isn't the same as the cached value
|
||||
@@ -225,6 +232,11 @@ namespace AZ
|
||||
{
|
||||
AZ::RHI::CpuProfiler::Get()->SetProfilerEnabled(keepDrawing);
|
||||
}
|
||||
|
||||
if (m_showVisualizer)
|
||||
{
|
||||
DrawVisualizer(m_showVisualizer, currentCpuTimingStatistics);
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::UpdateGroupRegionMap()
|
||||
@@ -251,5 +263,519 @@ namespace AZ
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -- CPU Visualizer --
|
||||
inline void ImGuiCpuProfiler::DrawVisualizer(bool& keepDrawing, const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics)
|
||||
{
|
||||
ImGui::SetNextWindowSize({ 900, 600 }, ImGuiCond_Once);
|
||||
if (ImGui::Begin("CPU Visualizer", &keepDrawing, ImGuiWindowFlags_None))
|
||||
{
|
||||
// Get the instrumentation data for the last frame if active
|
||||
if (!m_paused && m_groupRegionMap.size() != 0)
|
||||
{
|
||||
CollectFrameData(); // Also updates viewport bounds
|
||||
|
||||
CullFrameData(currentCpuTimingStatistics); // Trim data if necessary
|
||||
|
||||
if (!SystemTickBus::Handler::BusIsConnected())
|
||||
{
|
||||
SystemTickBus::Handler::BusConnect();
|
||||
}
|
||||
}
|
||||
|
||||
// Options & Statistics
|
||||
if (ImGui::BeginChild("Options and Statistics", { 0, 0 }, true))
|
||||
{
|
||||
ImGui::Columns(3, "Options", true);
|
||||
ImGui::Text("Frames To Collect:");
|
||||
ImGui::SliderInt("", &m_framesToCollect, 10, 100, "%d", ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic);
|
||||
|
||||
ImGui::NextColumn();
|
||||
|
||||
ImGui::Text("Viewport width: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(GetViewportTickWidth()));
|
||||
ImGui::Text("Ticks [%lld , %lld]", m_viewportStartTick, m_viewportEndTick);
|
||||
ImGui::Text("Recording %ld threads", RHI::CpuProfiler::Get()->GetTimeRegionMap().size());
|
||||
ImGui::Text("%llu profiling events saved", m_savedRegionCount);
|
||||
|
||||
ImGui::NextColumn();
|
||||
|
||||
ImGui::TextWrapped(
|
||||
"Hold the right mouse button to move around. Zoom by scrolling the mouse wheel while holding <ctrl>.");
|
||||
}
|
||||
|
||||
|
||||
ImGui::Columns(1, "RulerColumn", true);
|
||||
|
||||
// Ruler
|
||||
if (ImGui::BeginChild("Ruler", { 0, 30 }, true, ImGuiWindowFlags_NoNavFocus))
|
||||
{
|
||||
DrawRuler();
|
||||
}
|
||||
ImGui::EndChild();
|
||||
|
||||
|
||||
ImGui::Columns(1, "TimelineColumn", true);
|
||||
|
||||
// Timeline
|
||||
if (ImGui::BeginChild(
|
||||
"Timeline", { 0, 0 }, true, ImGuiWindowFlags_AlwaysVerticalScrollbar | ImGuiWindowFlags_NoScrollWithMouse))
|
||||
{
|
||||
// Find the next frame boundary after the viewport's right bound and draw until that tick
|
||||
auto nextFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportEndTick);
|
||||
if (nextFrameBoundaryItr == m_frameEndTicks.end() &&
|
||||
m_frameEndTicks.size() != 0) // lower_bound returns end() if not found
|
||||
{
|
||||
nextFrameBoundaryItr--;
|
||||
}
|
||||
const AZStd::sys_time_t nextFrameBoundary = *nextFrameBoundaryItr;
|
||||
|
||||
// Find the start tick of the leftmost frame, which may be offscreen.
|
||||
auto startTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick);
|
||||
if (startTickItr != m_frameEndTicks.begin())
|
||||
{
|
||||
startTickItr--;
|
||||
}
|
||||
|
||||
// Main draw loop
|
||||
u64 baseRow = 0;
|
||||
for (const auto& [currentThreadId, singleThreadData] : m_savedData)
|
||||
{
|
||||
// Find the first TimeRegion that we should draw
|
||||
auto regionItr = AZStd::lower_bound(
|
||||
singleThreadData.begin(), singleThreadData.end(), *startTickItr,
|
||||
[](const TimeRegion& wrapper, AZStd::sys_time_t target)
|
||||
{
|
||||
return wrapper.m_startTick < target;
|
||||
});
|
||||
|
||||
// Draw all of the blocks for a given thread/row
|
||||
u64 maxDepth = 0;
|
||||
while (regionItr != singleThreadData.end())
|
||||
{
|
||||
const TimeRegion& region = *regionItr;
|
||||
|
||||
// Early out if we have drawn all the onscreen regions
|
||||
if (region.m_startTick > nextFrameBoundary)
|
||||
{
|
||||
break;
|
||||
}
|
||||
u64 targetRow = region.m_stackDepth + baseRow;
|
||||
maxDepth = AZStd::max(aznumeric_cast<u64>(region.m_stackDepth), maxDepth);
|
||||
|
||||
DrawBlock(region, targetRow);
|
||||
|
||||
regionItr++;
|
||||
}
|
||||
|
||||
// Draw UI details
|
||||
DrawThreadLabel(baseRow, currentThreadId);
|
||||
DrawThreadSeparator(baseRow, maxDepth);
|
||||
|
||||
baseRow += maxDepth + 1; // Next draw loop should start one row down
|
||||
}
|
||||
|
||||
DrawRegionStatistics();
|
||||
DrawFrameBoundaries();
|
||||
|
||||
// Draw an invisible button to capture inputs
|
||||
ImGui::InvisibleButton("Timeline Input", { ImGui::GetWindowContentRegionWidth(), baseRow * RowHeight });
|
||||
|
||||
// Controls
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
if (ImGui::IsWindowFocused() && ImGui::IsItemHovered())
|
||||
{
|
||||
io.WantCaptureMouse = true;
|
||||
if (ImGui::IsMouseDragging(ImGuiMouseButton_Right)) // Scrolling
|
||||
{
|
||||
auto [deltaX, deltaY] = io.MouseDelta;
|
||||
if (deltaX != 0 || deltaY != 0)
|
||||
{
|
||||
// We want to maintain uniformity in scrolling (a click and drag should leave the cursor at the same spot
|
||||
// relative to the objects on screen)
|
||||
const float pixelDeltaNormalized = deltaX / ImGui::GetWindowWidth();
|
||||
auto tickDelta = aznumeric_cast<AZStd::sys_time_t>(-1 * pixelDeltaNormalized * GetViewportTickWidth());
|
||||
m_viewportStartTick += tickDelta;
|
||||
m_viewportEndTick += tickDelta;
|
||||
|
||||
ImGui::SetScrollY(ImGui::GetScrollY() + deltaY * -1);
|
||||
}
|
||||
}
|
||||
else if (io.MouseWheel != 0 && io.KeyCtrl) // Zooming
|
||||
{
|
||||
// We want zooming to be relative to the mouse's current position
|
||||
const float mouseVel = io.MouseWheel;
|
||||
const float mouseX = ImGui::GetMousePos().x;
|
||||
|
||||
// Find the normalized position of the cursor relative to the window
|
||||
const float percentWindow = (mouseX - ImGui::GetWindowPos().x) / ImGui::GetWindowWidth();
|
||||
|
||||
const auto overallTickDelta = aznumeric_cast<AZStd::sys_time_t>(0.05 * io.MouseWheel * GetViewportTickWidth());
|
||||
|
||||
// Split the overall delta between the two bounds depending on mouse pos
|
||||
const auto newStartTick = m_viewportStartTick + aznumeric_cast<AZStd::sys_time_t>(percentWindow * overallTickDelta);
|
||||
const auto newEndTick = m_viewportEndTick - aznumeric_cast<AZStd::sys_time_t>((1-percentWindow) * overallTickDelta);
|
||||
|
||||
// Avoid zooming too much, start tick should always be less than end tick
|
||||
if (newStartTick < newEndTick)
|
||||
{
|
||||
m_viewportStartTick = newStartTick;
|
||||
m_viewportEndTick = newEndTick;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::CollectFrameData()
|
||||
{
|
||||
const RHI::CpuProfiler::TimeRegionMap& timeRegionMap = RHI::CpuProfiler::Get()->GetTimeRegionMap();
|
||||
|
||||
m_viewportStartTick = INT64_MAX;
|
||||
m_viewportEndTick = INT64_MIN;
|
||||
|
||||
// Iterate through the entire TimeRegionMap and copy the data since it will get deleted on the next frame
|
||||
for (const auto& [threadId, singleThreadRegionMap] : timeRegionMap)
|
||||
{
|
||||
// The profiler can sometime return threads without any profiling events when dropping threads, FIXME(ATOM-15949)
|
||||
if (singleThreadRegionMap.size() == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now focus on just the data for the current thread
|
||||
AZStd::vector<TimeRegion> newData;
|
||||
newData.reserve(singleThreadRegionMap.size()); // Avoids reallocation in the normal case when each region only has one invocation
|
||||
for (const auto& [regionName, regionVec] : singleThreadRegionMap)
|
||||
{
|
||||
for (const TimeRegion& region : regionVec)
|
||||
{
|
||||
newData.push_back(region); // Copies
|
||||
|
||||
// Update running statistics if we want to record this region's data
|
||||
if (m_regionStatisticsMap[region.m_groupRegionName].m_record)
|
||||
{
|
||||
m_regionStatisticsMap[region.m_groupRegionName].RecordRegion(region);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sorting by start tick allows us to speed up some other processes (ex. finding the first block to draw)
|
||||
// since we can binary search by start tick.
|
||||
AZStd::sort(
|
||||
newData.begin(), newData.end(),
|
||||
[](const TimeRegion& lhs, const TimeRegion& rhs)
|
||||
{
|
||||
return lhs.m_startTick < rhs.m_startTick;
|
||||
});
|
||||
|
||||
// Use the latest frame's data as the new bounds of the viewport
|
||||
m_viewportStartTick = AZStd::min(newData.front().m_startTick, m_viewportStartTick);
|
||||
m_viewportEndTick = AZStd::max(newData.back().m_endTick, m_viewportEndTick);
|
||||
|
||||
m_savedRegionCount += newData.size();
|
||||
|
||||
// Move onto the end of the current thread's saved data, sorted order maintained
|
||||
AZStd::vector<TimeRegion>& savedDataVec = m_savedData[threadId];
|
||||
savedDataVec.insert(
|
||||
savedDataVec.end(), AZStd::make_move_iterator(newData.begin()), AZStd::make_move_iterator(newData.end()));
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::CullFrameData(const AZ::RHI::CpuTimingStatistics& currentCpuTimingStatistics)
|
||||
{
|
||||
const AZStd::sys_time_t frameToFrameTime = currentCpuTimingStatistics.m_frameToFrameTime;
|
||||
const AZStd::sys_time_t deleteBeforeTick = AZStd::GetTimeNowTicks() - frameToFrameTime * m_framesToCollect;
|
||||
|
||||
// Remove old frame boundary data
|
||||
auto firstBoundaryToKeepItr = AZStd::upper_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), deleteBeforeTick);
|
||||
m_frameEndTicks.erase(m_frameEndTicks.begin(), firstBoundaryToKeepItr);
|
||||
|
||||
// Remove old region data for each thread
|
||||
for (auto& [threadId, savedRegions] : m_savedData)
|
||||
{
|
||||
AZStd::size_t sizeBeforeRemove = savedRegions.size();
|
||||
|
||||
auto firstRegionToKeep = AZStd::lower_bound(
|
||||
savedRegions.begin(), savedRegions.end(), deleteBeforeTick,
|
||||
[](const TimeRegion& region, AZStd::sys_time_t target)
|
||||
{
|
||||
return region.m_startTick < target;
|
||||
});
|
||||
savedRegions.erase(savedRegions.begin(), firstRegionToKeep);
|
||||
|
||||
m_savedRegionCount -= sizeBeforeRemove - savedRegions.size();
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawBlock(const TimeRegion& block, u64 targetRow)
|
||||
{
|
||||
float wy = ImGui::GetWindowPos().y - ImGui::GetScrollY();
|
||||
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
const float startPixel = ConvertTickToPixelSpace(block.m_startTick);
|
||||
const float endPixel = ConvertTickToPixelSpace(block.m_endTick);
|
||||
|
||||
const ImVec2 startPoint = { startPixel, wy + targetRow * RowHeight };
|
||||
const ImVec2 endPoint = { endPixel, wy + targetRow * RowHeight + 40 };
|
||||
|
||||
const ImU32 blockColor = GetBlockColor(block);
|
||||
|
||||
drawList->AddRectFilled(startPoint, endPoint, blockColor, 0);
|
||||
|
||||
// Draw the region name if possible
|
||||
// If the block's current width is too small, we skip drawing the label.
|
||||
const float regionPixelWidth = endPixel - startPixel;
|
||||
const float maxCharWidth = ImGui::CalcTextSize("M").x; // M is usually the largest character in most fonts (see CSS em)
|
||||
if (regionPixelWidth > maxCharWidth) // We can draw at least one character
|
||||
{
|
||||
const AZStd::string label =
|
||||
AZStd::string::format("%s/ %s", block.m_groupRegionName->m_groupName, block.m_groupRegionName->m_regionName);
|
||||
const float textWidth = ImGui::CalcTextSize(label.c_str()).x;
|
||||
|
||||
if (regionPixelWidth < textWidth) // Not enough space in the block to draw the whole name, draw clipped text.
|
||||
{
|
||||
// clipRect appears to only clip when a character is fully outside of its bounds which can lead to overflow
|
||||
// for now subtract the width of a character
|
||||
const ImVec4 clipRect = { startPoint.x, startPoint.y, endPoint.x - maxCharWidth, endPoint.y };
|
||||
const float fontSize = ImGui::GetFont()->FontSize;
|
||||
|
||||
ImGui::GetFont()->RenderText(drawList, fontSize, startPoint, IM_COL32_WHITE, clipRect, label.c_str(), 0);
|
||||
}
|
||||
else // We have enough space to draw the entire label, draw and center text.
|
||||
{
|
||||
const float remainingWidth = regionPixelWidth - textWidth;
|
||||
const float offset = remainingWidth * .5;
|
||||
|
||||
drawList->AddText({ startPoint.x + offset, startPoint.y }, IM_COL32_WHITE, label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// Tooltip and block highlighting
|
||||
if (ImGui::IsMouseHoveringRect(startPoint, endPoint) && ImGui::IsWindowHovered())
|
||||
{
|
||||
// Open function statistics map on click
|
||||
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left))
|
||||
{
|
||||
const GroupRegionName* key = block.m_groupRegionName;
|
||||
m_regionStatisticsMap[key].m_draw = true;
|
||||
}
|
||||
|
||||
// Hovering outline
|
||||
drawList->AddRect(startPoint, endPoint, ImGui::GetColorU32({ 1, 1, 1, 1 }), 0.0, 0, 1.5);
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::Text("%s::%s", block.m_groupRegionName->m_groupName, block.m_groupRegionName->m_regionName);
|
||||
ImGui::Text("Execution time: %.3f ms", CpuProfilerImGuiHelper::TicksToMs(block.m_endTick - block.m_startTick));
|
||||
ImGui::Text("Ticks %lld => %lld", block.m_startTick, block.m_endTick);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
|
||||
inline ImU32 ImGuiCpuProfiler::GetBlockColor(const TimeRegion& block)
|
||||
{
|
||||
// Use the GroupRegionName pointer a key into the cache, equal regions will have equal pointers
|
||||
const GroupRegionName* key = block.m_groupRegionName;
|
||||
if (m_regionColorMap.contains(key)) // Cache hit
|
||||
{
|
||||
return ImGui::GetColorU32(m_regionColorMap[key]);
|
||||
}
|
||||
|
||||
// Cache miss, generate a new random color
|
||||
AZ::SimpleLcgRandom rand(aznumeric_cast<u64>(AZStd::GetTimeNowTicks()));
|
||||
const float r = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f);
|
||||
const float g = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f);
|
||||
const float b = AZStd::clamp(rand.GetRandomFloat(), .1f, .9f);
|
||||
const ImVec4 randomColor = {r, g, b, .8};
|
||||
m_regionColorMap.emplace(key, randomColor);
|
||||
return ImGui::GetColorU32(randomColor);
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawThreadSeparator(u64 baseRow, u64 maxDepth)
|
||||
{
|
||||
const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 });
|
||||
|
||||
auto [wx, wy] = ImGui::GetWindowPos();
|
||||
wy -= ImGui::GetScrollY();
|
||||
const float windowWidth = ImGui::GetWindowWidth();
|
||||
const float boundaryY = wy + (baseRow + maxDepth + 1) * RowHeight - 5;
|
||||
|
||||
ImGui::GetWindowDrawList()->AddLine({ wx, boundaryY }, { wx + windowWidth, boundaryY }, red, 2.0f);
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawThreadLabel(u64 baseRow, AZStd::thread_id threadId)
|
||||
{
|
||||
auto [wx, wy] = ImGui::GetWindowPos();
|
||||
wy -= ImGui::GetScrollY();
|
||||
const AZStd::string threadIdText = AZStd::string::format("Thread %zu", static_cast<size_t>(threadId.m_id));
|
||||
|
||||
ImGui::GetWindowDrawList()->AddText({ wx + 10, wy + baseRow * RowHeight + 5 }, IM_COL32_WHITE, threadIdText.c_str());
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawRegionStatistics()
|
||||
{
|
||||
for (auto& [groupRegionName, stat] : m_regionStatisticsMap)
|
||||
{
|
||||
if (stat.m_draw)
|
||||
{
|
||||
ImGui::SetNextWindowSize({300, 340}, ImGuiCond_FirstUseEver);
|
||||
ImGui::Begin(groupRegionName->m_regionName, &stat.m_draw, 0);
|
||||
|
||||
if (ImGui::Button(stat.m_record ? "Pause" : "Resume"))
|
||||
{
|
||||
stat.m_record = !stat.m_record;
|
||||
}
|
||||
|
||||
ImGui::Text("Invocations: %llu", stat.m_invocations);
|
||||
ImGui::Text("Average time: %.3f ms", stat.CalcAverageTimeMs());
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
ImGui::ColorPicker4("Region color", &m_regionColorMap[groupRegionName].x);
|
||||
ImGui::End();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawFrameBoundaries()
|
||||
{
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
const float wy = ImGui::GetWindowPos().y;
|
||||
const float windowHeight = ImGui::GetWindowHeight();
|
||||
const ImU32 red = ImGui::GetColorU32({ 1, 0, 0, 1 });
|
||||
|
||||
// End ticks are sorted in increasing order, find the first frame bound to draw
|
||||
auto endTickItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick);
|
||||
|
||||
while (endTickItr != m_frameEndTicks.end() && *endTickItr < m_viewportEndTick)
|
||||
{
|
||||
const float horizontalPixel = ConvertTickToPixelSpace(*endTickItr);
|
||||
drawList->AddLine({ horizontalPixel, wy }, { horizontalPixel, wy + windowHeight }, red);
|
||||
endTickItr++;
|
||||
}
|
||||
}
|
||||
|
||||
inline void ImGuiCpuProfiler::DrawRuler()
|
||||
{
|
||||
// Use a pair of iterators to go through all saved frame boundaries and draw ruler lines
|
||||
auto lastFrameBoundaryItr = AZStd::lower_bound(m_frameEndTicks.begin(), m_frameEndTicks.end(), m_viewportStartTick);
|
||||
auto nextFrameBoundaryItr = lastFrameBoundaryItr;
|
||||
if (lastFrameBoundaryItr != m_frameEndTicks.begin())
|
||||
{
|
||||
lastFrameBoundaryItr--;
|
||||
}
|
||||
|
||||
const auto [wx, wy] = ImGui::GetWindowPos();
|
||||
ImDrawList* drawList = ImGui::GetWindowDrawList();
|
||||
|
||||
while (nextFrameBoundaryItr != m_frameEndTicks.end())
|
||||
{
|
||||
const AZStd::sys_time_t lastFrameBoundaryTick = *lastFrameBoundaryItr;
|
||||
const AZStd::sys_time_t nextFrameBoundaryTick = *nextFrameBoundaryItr;
|
||||
if (lastFrameBoundaryTick > m_viewportEndTick)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
const float lastFrameBoundaryPixel = ConvertTickToPixelSpace(lastFrameBoundaryTick);
|
||||
const float nextFrameBoundaryPixel = ConvertTickToPixelSpace(nextFrameBoundaryTick);
|
||||
|
||||
const AZStd::string label =
|
||||
AZStd::string::format("%.2f ms", CpuProfilerImGuiHelper::TicksToMs(nextFrameBoundaryTick - lastFrameBoundaryTick));
|
||||
const float labelWidth = ImGui::CalcTextSize(label.c_str()).x;
|
||||
|
||||
// The label can fit between the two boundaries, center it and draw
|
||||
if (labelWidth <= nextFrameBoundaryPixel - lastFrameBoundaryPixel)
|
||||
{
|
||||
const float offset = (nextFrameBoundaryPixel - lastFrameBoundaryPixel - labelWidth) /2;
|
||||
const float textBeginPixel = lastFrameBoundaryPixel + offset;
|
||||
const float textEndPixel = textBeginPixel + labelWidth;
|
||||
|
||||
// Execution time label
|
||||
drawList->AddText({ textBeginPixel, wy + ImGui::GetWindowHeight() / 4 }, IM_COL32_WHITE, label.c_str());
|
||||
|
||||
// Left side
|
||||
drawList->AddLine(
|
||||
{ lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 },
|
||||
{ textBeginPixel - 5, wy + ImGui::GetWindowHeight() / 2},
|
||||
IM_COL32_WHITE);
|
||||
|
||||
// Right side
|
||||
drawList->AddLine(
|
||||
{ textEndPixel, wy + ImGui::GetWindowHeight()/2 },
|
||||
{ nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight()/2 },
|
||||
IM_COL32_WHITE);
|
||||
}
|
||||
else // Cannot fit inside, just draw a line between the two boundaries
|
||||
{
|
||||
drawList->AddLine(
|
||||
{ lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 },
|
||||
{ nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() / 2 },
|
||||
IM_COL32_WHITE);
|
||||
}
|
||||
|
||||
// Left bound
|
||||
drawList->AddLine(
|
||||
{ lastFrameBoundaryPixel, wy },
|
||||
{ lastFrameBoundaryPixel, wy + ImGui::GetWindowHeight() },
|
||||
IM_COL32_WHITE);
|
||||
|
||||
// Right bound
|
||||
drawList->AddLine(
|
||||
{ nextFrameBoundaryPixel, wy },
|
||||
{ nextFrameBoundaryPixel, wy + ImGui::GetWindowHeight() },
|
||||
IM_COL32_WHITE);
|
||||
|
||||
lastFrameBoundaryItr = nextFrameBoundaryItr;
|
||||
nextFrameBoundaryItr++;
|
||||
}
|
||||
}
|
||||
|
||||
inline AZStd::sys_time_t ImGuiCpuProfiler::GetViewportTickWidth() const
|
||||
{
|
||||
return m_viewportEndTick - m_viewportStartTick;
|
||||
}
|
||||
|
||||
inline float ImGuiCpuProfiler::ConvertTickToPixelSpace(AZStd::sys_time_t tick) const
|
||||
{
|
||||
const float wx = ImGui::GetWindowPos().x;
|
||||
const float tickSpaceShifted = aznumeric_cast<float>(tick - m_viewportStartTick); // This will be close to zero, so FP inaccuracy should not be too bad
|
||||
const float tickSpaceNormalized = tickSpaceShifted / GetViewportTickWidth();
|
||||
const float pixelSpace = tickSpaceNormalized * ImGui::GetWindowWidth() + wx;
|
||||
return pixelSpace;
|
||||
}
|
||||
|
||||
// System tick bus overrides
|
||||
inline void ImGuiCpuProfiler::OnSystemTick()
|
||||
{
|
||||
m_frameEndTicks.push_back(AZStd::GetTimeNowTicks());
|
||||
|
||||
if (!m_showVisualizer || m_paused)
|
||||
{
|
||||
SystemTickBus::Handler::BusDisconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// ----- RegionStatistics implementation -----
|
||||
|
||||
inline float RegionStatistics::CalcAverageTimeMs() const
|
||||
{
|
||||
if (m_invocations == 0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
const double averageTicks = aznumeric_cast<double>(m_totalTicks) / m_invocations;
|
||||
return CpuProfilerImGuiHelper::TicksToMs(aznumeric_cast<AZStd::sys_time_t>(averageTicks));
|
||||
}
|
||||
|
||||
inline void RegionStatistics::RecordRegion(const AZ::RHI::CachedTimeRegion& region)
|
||||
{
|
||||
m_invocations++;
|
||||
m_totalTicks += region.m_endTick - region.m_startTick;
|
||||
}
|
||||
} // namespace Render
|
||||
} // namespace AZ
|
||||
|
||||
Reference in New Issue
Block a user